Creating a Search in Slim Framework with hooks

My project website numphp.org is based on the PHP micro framework Slim. There is no nead for a cms or blog system. I just need some static pages and a little bit of logic. My posts on numphp.org are just static html files that will be served with the php function file_get_contents. But I would like to make the posts searchable. The easy way would be a slim route for search that is just checking if the searched query is contained in every post of my blog. But in near future I would like to make also the documentation searchable. So I will use the hooks of the Slim Framework. Hooks are the same as events in other frameworks. You can throw them and listen or observe them.

Two weeks ago I could not find events in the Slim Framework so I made a Pull Requests to add a EventManager to the Framework. But before sending the Pull Request I have figured out, events are just called hooks:)

Creating a Search Form in the Frontend

The form in the frontend looks like this. Don't be confused by all the css classes, I'm using bootstrap. The important part is the name query in the input and the action and method of the form.

    <form class="navbar-form navbar-right" action="/search" method="get" id="search-form">
        <div class="input-group">
            <input type="text" class="form-control" name="query" placeholder="Search for ..."/>
            <span class="input-group-btn">
                <button class="btn" type="submit" form="search-form"><i class="fa fa-search"></i></button>
            </span>
        </div>
    </form>

Creating a Search Route

The Search Route is created very quick. If you don't know how to build a route in the Slim Framework you can read it in the docs. But it's very simple, we just say the query starts with /search and we give the route a anonymous function how to handle the request.

$app->get(
    '/search',
    function () use ($app) {
        $query = $app->request->get('query');
        $hookObject = (object) ['query' => $query, 'results' => []];
        $app->applyHook('search', $hookObject);
        $app->render(
            'search.phtml',
            [
                'query' => $query,
                'results' => $hookObject->results,
            ]
        );
    }
);

A more detailed view on the code. We extract the get parameter query from the request.

$query = $app->request->get('query');

And now we build a object that will contain the $query and the search results. At this point just an empty array.

$hookObject = (object) ['query' => $query, 'results' => []];

Now we can applyHook with the key search and the $hookObkect. Every registerd hook function can now edit the search result array.

$app->applyHook('search', $hookObject);

At this point we will switch to an other point of our application where we hook the applied Hook search.

Observing the Hook

Now we can listen to this hook everywhere in our application. We just apply our blog posts to the search result.

$app->hook('search', function ($hookObject) use ($posts) {
    $query = $hookObject->query;
    $result = [];
    foreach ($posts as $post) {
        $content = strip_tags(
            file_get_contents(__DIR__ . '/../templates/blog/' . $post['slug'] . '.html')
        );
        if (stripos($content, $query) !== false || stripos($post['title'], $query) !== false) {
            $result[] = ['title' => $post['title'], 'uri' => '/blog/' . $post['slug']];
        }
    }
    if (count($result)) {
        $hookObject->results['Blog'] = $result;
    }
});

The function hook is listening to the hook search and will call our anonymous function with parameters that we have injected into applyHook before. You may ask what the variable $posts is. That is something like my config array for my posts and it looks like this.

$posts = [
    [
        'title' => 'Cholesky decomposition in PHP',
        'created' => new DateTime('2015-01-16'),
        'slug' => 'cholesky_decomposition_in_php',
    ],
    [
        'title' => 'Calculate the inverse of a matrix in PHP',
        'created' => new DateTime('2015-01-14'),
        'slug' => 'calculate_the_inverse_of_a_matrix_in_php',
    ],
];

As you can see above, we just search very simple with stripos in the content of our posts if the given query is contained. If we have found one or two posts, we put them into an array with a title and a uri and append that array with the key Blog to the search results. That gives us the possibility to make different between search results from the blog and maybe later from the documentation.

All results collected

Now we have collected all results and are again in our search route from above. We take all collected results and render them.

        $app->render(
            'search.phtml',
            [
                'query' => $query,
                'results' => $hookObject->results,
            ]
        );

This can be the part of a template for the results.

<div class="jumbotron jumbotron-red">
    <div class="container">
        <h1><i class="fa fa-search"></i> Search</h1>
        <p class="lead">Here are the results for <code><?php echo htmlspecialchars($query); ?></code></p>
    </div>
</div>
<div class="container container-numphp">
    <?php foreach($results as $key => $result): ?>
        <h2 class="page-header"><?php echo $key; ?></h2>
        <?php foreach($result as $entry): ?>
            <h3>
                <a href="<?php echo $entry['uri']; ?>" title="<?php echo $entry['title']; ?>"><?php echo $entry['title']; ?></a>
            </h3>
        <?php endforeach; ?>
    <?php endforeach; ?>
</div>

Please don't forget to escape the $query before sending it. All other variables are just out of my static PHP or Html files and no problem.

Next Previous