The Silex microframework

After a taste of what Laravel can offer you, you most likely do not want to hear about minimalist microframeworks. Still, we think it is good to know more than one framework. You can get to know different approaches, be more versatile, and everyone will want you in their team.

We chose Silex because it is a microframework, which is very different from Laravel, and also because it is part of the Symfony family. With this introduction to Silex, you will learn how to use your second framework, which is of a totally different type, and you will be one step closer to knowing Symfony as well, which is one of the big players.

What is the benefit of microframeworks? Well, they provide the very basics—that is, a router, a simple dependency injector, request helpers, and so on, but this is the end of it. You have plenty of room to choose and build what you really need, including external libraries or even your own ones. This means that you can have a framework specially customized for each different project. In fact, Silex provides a handful of built-in service providers that you can integrate very easily, from template engines to logging or security.

Installation

There's no news here. Composer does everything for you, as it does with Laravel. Execute the following command on your command line at the root of your new project in order to include Silex in your composer.json file:

$ composer require silex/silex

You may require more dependencies, but let's add them when we need them.

Project setup

Silex's most important class is SilexApplication. This class, which extends from Pimple (a lightweight dependency injector), manages almost anything. You can use it as an array as it implements the ArrayAccess interface, or you could invoke its methods to add dependencies, register services, and so on. The first thing to do is to instantiate it in your public/index.php file, as follows:

<?php

use SilexApplication;

require_once __DIR__ . '/../vendor/autoload.php';

$app = new Application();

Managing configuration

One of the first things we like to do is load the configuration. We could do something very simple, such as including a file with PHP or JSON content, but let's make use of one of the service providers, ConfigServiceProvider. Let's add it with Composer via the following line:

$ composer require igorw/config-service-provider

This service allows us to have multiple configuration files, one for each environment we need. Imagining that we want to have two environments, prod and dev, this means we need two files: one in config/prod.json and one in config/dev.json. The config/dev.json file would look similar to this:

{
  "debug": true,
  "cache": false,
  "database": {
    "user": "dev",
    "password": ""
  }
}

The config/prod.json file would look similar to this:

{
  "debug": false,
  "cache": true,
  "database ": {
    "user": "root",
    "password": "fsd98na9nc"
  }
}

In order to work in a development environment, you will need to set the correct value to the environment variable by running the following command:

export APP_ENV=dev

The APP_ENV environment variable will be the one telling us which environment we are in. Now, it is time to use this service provider. In order to register it by reading from the configuration file of the current environment, add the following lines to your index.php file:

$env = getenv('APP_ENV') ?: 'prod';
$app->register(
    new IgorwSilexConfigServiceProvider(
        __DIR__ . "/../config/$env.json"
    )
);

The first thing we did here is to get the environment from the environment variable. By default, we set it to prod. Then, we invoked register from the $app object to add an instance of ConfigServiceProvider by passing the correct configuration file path. From now on, the $app "array" will contain three entries: debug, cache, and db with the content of the configuration files. We will be able to access them whenever we have access to $app, which will be mostly everywhere.

Setting the template engine

Another of the handy service providers is Twig. As you might remember, Twig is the template engine that we used in our own framework, and it is, in fact, from the same people that developed Symfony and Silex. You also already know how to add the dependency with Composer; simply run the following:

$ composer require twig/twig

To register the service, we will need to add the following lines in our public/index.php file:

$app->register(
    new SilexProviderTwigServiceProvider(),
    ['twig.path' => __DIR__ . '/../views']
);

Also, create the views/ directory where we will later store our templates. Now, you have the Twig_Environment instance available by just accessing $app['twig'].

Adding a logger

The last one of the service providers that we will register for now is the logger. This time, the library to use is Monolog, and you can include this via the following:

$ composer require monolog/monolog

The quickest way to register a service is by just providing the path of the log file, which can be done as follows:

$app->register(
    new SilexProviderMonologServiceProvider(),
    ['monolog.logfile' => __DIR__ . '/../app.log']
);

If you would like to add more information to this service provider, such as what level of logs you want to save, the name of the log, and so on, you can add them to the array together with the log file. Take a look at the documentation at http://silex.sensiolabs.org/doc/providers/monolog.html for the full list of parameters available.

As with the template engine, from now on, you can access the MonologLogger instance from the Application object by accessing $app['monolog'].

Adding the first endpoint

It is time to see how the router works in Silex. We would like to add a simple endpoint for the home page. As we already mentioned, the $app instance can manage almost anything, including routes. Add the following code at the end of the public/index.php file:

$app->get('/', function(Application $app) {
    return $app['twig']->render('home.twig');
});

This is a similar way of adding routes to the one that Laravel follows. We invoked the get method as it is a GET endpoint, and we passed the route string and the Application instance. As we mentioned here, $app also acts as a dependency injector—in fact, it extends from one: Pimple—so you will notice the Application instance almost everywhere. The result of the anonymous function will be the response that we will send to the user—in this case, a rendered Twig template.

Right now, this will not do the trick. In order to let Silex know that you are done setting up your application, you need to invoke the run method at the very end of the public/index.php file. Remember that if you need to add anything else to this file, it has to be before this line:

$app->run();

You have already worked with Twig, so we will not spend too much time on this. The first thing to add is the views/home.twig template:

{% extends "layout.twig" %}

{% block content %}
    <h1>Hi visitor!</h1>
{% endblock %}

Now, as you might have already guessed, we will add the views/layout.twig template, as follows:

<html>
<head>
    <title>Silex Example</title>
</head>
<body>
{% block content %}
{% endblock %}
</body>
</html>

Try accessing the home page of your application; you should get the following result:

Adding the first endpoint

Accessing the database

For this section, we will write an endpoint that will create recipes for our cookbook. Run the following MySQL queries in order to set up the cookbook database and create the empty recipes table:

mysql> CREATE SCHEMA cookbook;
Query OK, 1 row affected (0.00 sec)
mysql> USE cookbook;
Database changed
mysql> CREATE TABLE recipes(
    -> id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
    -> name VARCHAR(255) NOT NULL,
    -> ingredients TEXT NOT NULL,
    -> instructions TEXT NOT NULL,
    -> time INT UNSIGNED NOT NULL);
Query OK, 0 rows affected (0.01 sec)

Silex does not come with any ORM integration, so you will need to write your SQL queries by hand. However, there is a Doctrine service provider that gives you a simpler interface than the one PDO offers, so let's try to integrate it. To install this, run the following command:

$ composer require "doctrine/dbal:~2.2"

Now, we are ready to register the service provider. As with the rest of services, add the following code to your public/index.php before the route definitions:

$app->register(new SilexProviderDoctrineServiceProvider(), [
    'dbs.options' => [
        [
            'driver'    => 'pdo_mysql',
            'host'      => '127.0.0.1',
            'dbname'    => 'cookbook',
            'user'      => $app['database']['user'],
            'password'  => $app['database']['password']
        ]
    ]
]);

When registering, you need to provide the options for the database connection. Some of them will be the same regardless of the environment, such as the driver or even the host, but some will come from the configuration file, such as $app['database']['user']. From now on, you can access the database connection via $app['db'].

With the database set up, let's add the routes that will allow us to add and fetch recipes. As with Laravel, you can specify either the anonymous function, as we already did, or a controller and method to execute. Replace the current route with the following three routes:

$app->get(
    '/',
    'CookBook\Controllers\RecipesController::getAll'
);
$app->post(
    '/recipes',
    'CookBook\Controllers\RecipesController::create'
);
$app->get(
    '/recipes',
    'CookBook\Controllers\RecipesController::getNewForm'
);

As you can observe, there will be a new controller, CookBookControllersRecipesController, which will be placed in src/Controllers/RecipesController.php. This means that you need to change the autoloader in Composer. Edit your composer.json file with the following:

"autoload": {
    "psr-4": {"CookBook\": "src/"}
}

Now, let's add the controller class, as follows:

<?php

namespace CookBookControllers;

class Recipes {
    
}

The first method we will add is the getNewForm method, which will just render the add a new recipe page. The method looks similar to this:

public function getNewForm(Application $app): string {
    return $app['twig']->render('new_recipe.twig');
}

The method will just render new_recipe.twig. An example of this template could be as follows:

{% extends "layout.twig" %}

{% block content %}
    <h1>Add recipe</h1>
    <form method="post">
        <div>
            <label for="name">Name</label>
            <input type="text" name="name"
                   value="{{ name is defined ? name : "" }}" />
        </div>
        <div>
            <label for="ingredients">Ingredients</label>
            <textarea name="ingredients">
                {{ ingredients is defined ? ingredients : "" }}
            </textarea>
        </div>
        <div>
            <label for="instructions">Instructions</label>
            <textarea name="instructions">
                {{ instructions is defined ? instructions : "" }}
            </textarea>
        </div>
        <div>
            <label for="time">Time (minutes)</label>
            <input type="number" name="time"
                   value="{{ time is defined ? time : "" }}" />
        </div>
        <div>
            <button type="submit">Save</button>
        </div>
    </form>
{% endblock %}

This template sends the name, ingredients, instructions, and the time that it takes to prepare the dish. The endpoint that will get this form needs to get the response object in order to extract this information. In the same way that we could get the Application instance as an argument, we can get the Request one too if we specify it in the method definition. Accessing the POST parameters is as easy as invoking the get method by sending the name of the parameter or calling $request->request->all() to get all of them as an array. Add the following method that checks whether all the data is valid and renders the form again if it is not, sending the submitted data and errors:

public function create(Application $app, Request $request): string {
    $params = $request->request->all();
    $errors = [];

    if (empty($params['name'])) {
        $errors[] = 'Name cannot be empty.';
    }
    if (empty($params['ingredients'])) {
        $errors[] = 'Ingredients cannot be empty.';
    }
    if (empty($params['instructions'])) {
        $errors[] = 'Instructions cannot be empty.';
    }
    if ($params['time'] <= 0) {
        $errors[] = 'Time has to be a positive number.';
    }
    
    if (!empty($errors)) {
        $params = array_merge($params, ['errors' => $errors]);
        return $app['twig']->render('new_recipe.twig', $params);
    }
}

The layout.twig template needs to be edited too in order to show the errors returned. We can do this by executing the following:

{# ... #}
{% if errors is defined %}
    <p>Something went wrong!</p>
    <ul>
    {% for error in errors %}
        <li>{{ error }}</li>
    {% endfor %}
    </ul>
{% endif %}
{% block content %}
{# ... #}

At this point, you can already try to access http://localhost/recipes, fill the form leaving something empty, submitting, and getting the form back with the errors. It should look something similar to this (with some extra CSS styles):

Accessing the database

The continuation of the controller should allow us to store the correct data as a new recipe in the database. To do so, it would be a good idea to create a separate class, such as CookBookModelsRecipeModel; however, to speed things up, let's add the following few lines that would go into the model to the controller. Remember that we have the Doctrine service provider, so there is no need to use PDO directly:

$sql = 'INSERT INTO recipes (name, ingredients, instructions, time) '
    . 'VALUES(:name, :ingredients, :instructions, :time)';
$result = $app['db']->executeUpdate($sql, $params);

if (!$result) {
    $params = array_merge($params, ['errors' => $errors]);
    return $app['twig']->render('new_recipe.twig', $params);
}

return $app['twig']->render('home.twig');

Doctrine also helps when fetching data. To see it working, check the third and final method, in which we will fetch all the recipes in order to show the user:

public function getAll(Application $app): string {
    $recipes = $app['db']->fetchAll('SELECT * FROM recipes');
    return $app['twig']->render(
        'home.twig',
        ['recipes' => $recipes]
    );
}

With only one line, we performed a query. It is not as clean as the Eloquent ORM of Laravel, but at least it is much less verbose than using raw PDO. Finally, you can update your home.twig template with the following content in order to display the recipes that we just fetched from the database:

{% extends "layout.twig" %}

{% block content %}
    <h1>Hi visitor!</h1>
    <p>Check our recipes!</p>
    <table>
        <th>Name</th>
        <th>Time</th>
        <th>Ingredients</th>
        <th>Instructions</th>
    {% for recipe in recipes %}
        <tr>
            <td>{{ recipe.name }}</td>
            <td>{{ recipe.time }}</td>
            <td>{{ recipe.ingredients }}</td>
            <td>{{ recipe.instructions }}</td>
        </tr>
    {% endfor %}
    </table>
{% endblock %}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.255.168