Build simple REST API with PHP – Part 3

This is the last part of our post serie about building simple REST API with PHP. We can find the two previous ones here:

  1. Part 1
  2. Part 2

In the last part we’ll add Middlewares to add more features to our API and also customize some error outputs to make them more consumable.

 

Middlewares

We will add the following three middlewares:

  • ContentTypes: Parses JSON-formatted body from the client (the innermost);
  • JSON: utility middleware for “JSON only responses” and “JSON encoded bodies”;
  • Authentication (outermost).

 

ContentType Middleware

For the ContentType middleware we’ll use an existing Slim middleware, so we just need to add it in our bootstrap.php file:

1
2
3
4
...
// Middlewares
// Content Type (inner)
$app->add(new \Slim\Middleware\ContentTypes());

This middleware intercepts the HTTP request body and parses it into the appropriate PHP data structure if possible; else it returns the HTTP request body unchanged. This is particularly useful for preparing the HTTP request body for an XML or JSON API.

 

JSON Middleware

Our JSON middleware achieves two best practices: “JSON only responses” and “JSON encoded bodies”. So, lets add it in our bootstrap.php file:

1
$app->add(new API\Middleware\JSON());

As we can see if we review a classic Middleware class, the call() method of a Slim middleware is where the action takes place. Here is our JSON middleware class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace API\Middleware;

class JSON extends \Slim\Middleware
{
    public function call()
    {
        try {
            // Force response headers to JSON
            $this->app->response->headers->set('Content-Type', 'application/json');

            $method = strtolower($this->app->request->getMethod());
            $mediaType = $this->app->request->getMediaType();
            $body = $this->app->request()->getBody();
            // Validate JSON format
            if (!$this->app->isJSON($body)) {
                throw new \Exception("JSON data is malformed", 400);
            }
            if (in_array($method, array('post', 'put', 'patch')) && '' !== $body) {
                if (empty($mediaType) || $mediaType !== 'application/json') {
                    $this->app->halt(415);
                }
            }
        } catch (\Exception $e) {
            echo json_encode(array(
                    'code' => $e->getCode(),
                    'message' => $e->getMessage()
                ),
                JSON_PRETTY_PRINT
            );
            exit;
        }
    }

    $this->next->call();
}

If the request method is one of the write-enabled ones (PUT, POST, PATCH) the request content type header must be application/json, if not the application exits with a 415 Unsupported Media Type HTTP status code. If all is right the statement $this->next->call() runs the next middleware in the chain.

 

Authentication Middleware

To authenticate our api users we’ll use a third-party library: JWT Authentication Middleware for Slim

1
2
3
$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub"
]));

This middleware implements JSON Web Token Authentication for Slim Framework. It does not implement OAuth 2.0 authorization server nor does it provide ways to generate, issue or store authentication tokens. It only parses and authenticates a token when passed via header or cookie.

 

Pretty outputs for Errors

Our API should show useful error messages in pretty format. We need a minimal payload that contains an error code and message. With Slim we can redefine both 404 errors and server errors with the $app->notFound() and $app->error() method respectively.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$app->notFound(function () use ($app) {

    $mediaType = $app->request->getMediaType();
    $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());

    if ('application/json' === $mediaType || true === $isAPI) {

        $app->response->headers->set('Content-Type', 'application/json');

        echo json_encode(
            array(
                'code' => 404,
                'message' => 'Not found'
            )
        );

    }
});

For other errors we can use and customize the error() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$app->error(function (\Exception $e) use ($app, $log) {

    $mediaType = $app->request->getMediaType();
    $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());

    // Standard exception data
    $error = array(
        'code' => $e->getCode(),
        'message' => $e->getMessage()
    );

    // Custom error data (e.g. Validations)
    if (method_exists($e, 'getData')) {
        $errors = $e->getData();
    }
   
    if (!empty($errors)) {
        $error['errors'] = $errors;
    }

    $log->error($e->getMessage());
    if ('application/json' === $mediaType || true === $isAPI) {
        $app->response->headers->set('Content-Type', 'application/json');
        echo json_encode($error);
    }
});

The $app->error() method receives the thrown exception as argument and print it in a pretty way.

Well that’s, we’ve developed a basic API with common best practices. I hope these post series are helpful for you guys.

Build simple REST API with PHP – Part 2

In our first part of this post series we saw how to bootstrap our API and we defined the end points for it. Now, we’ll add the necessary logic to make our end points to work.

End Points

GET /users

Here we will get ALL our existing users.

1
2
3
4
5
6
7
8
9
10
11
12
13
// GET route for all users
$app->get('/users', function () use ($app, $log) {
    $users = array();
    $results = \ORM::forTable('user');

    $users = $results->findArray();

    // Here, we can add sorting, filtering and/or searching logic

    $output = array('code' => 200, 'data' => $users);
    echo json_encode($output, JSON_PRETTY_PRINT);
    return;
});

 

GET /users/:id

Given a user ID we return that user if exists, if not we return a 404 error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GET route
$app->get('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $user = $user->asArray();
           
        $output = array('code' => 200, 'data' => array($user));
        echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    }
   
    $app->notFound();
});

Observe how the “:id” url parameter is passed as an argument of the callable function.

 

POST /users

Now, sending POST data to the /users end point we will be able to create a new User.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// POST route, for creating
$app->post('/users', function () use ($app, $log) {
    // Get POSTed data in body
    $body = $app->request()->getBody();

    // Validate data here

    $user = \ORM::for_table('user')->create();
    $user->set($body);
    if ($user->save() === true) {
        $output = array(
            'code' => 200,
            'data' => array($user->asArray())
        );
    echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    } else {
        throw new \Exception('Something went wrong.');
    }
});

 

PUT /users/:id

Update an existing user with passed data given a user ID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// PUT route, for updating
$app->put('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $body = $app->request()->getBody();

        // Validate data here

        $user->set($body);
        if ($user->save() === true) {
            $user = $user->asArray();
           
            $output = array('code' => 200, 'data' => array($user));
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

DELETE /users/:id

Here given a user ID we will delete the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// DELETE route
$app->delete('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $user->delete();
        if ($user->save() === true) {
            $output = array('code' => 200, 'data' => array());
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

User’s Phonenumbers

Now, we will define the end points for our second collection Phonenumbers.

GET /users/:id/phonenumbers

In this end point we will return all the associated phonenumbers to a user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// GET route for all users's phonenumbers
$app->get('/users/:id/phonenumbers', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumbers = \ORM::forTable('phonenumber')
            ->where('user_id', $id)
            ->orderByDesc('id')
            ->findArray();
        $data['phonenumbers'] = $phonenumbers;
        $output = array('code' => 200, 'data' => $data);
        echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    }
   
    $app->notFound();
});

 

GET /users/:id/phonenumbers/:pid

Now given a user ID and a phone ID we will return that specific phonenumber for the given user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// GET route for specific user's phonenumber
$app->get('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumber = \ORM::forTable('phonenumber')
            ->where('user_id', $id)
            ->andWhere('id', $phoneId)
            ->findOne();
        if ($phonenumber) {
            $data['phonenumber'] = $phonenumber;
            $output = array('code' => 200, 'data' => $data);
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        }
    }
   
    $app->notFound();
});

 

POST /users/:id/phonenumbers

With this end point we will be able to create/add a phonenumber to a user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// POST route, for creating a new user's phonenumber
$app->post('/users/:id/phonenumbers', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        // Get POSTed data in body
        $body = $app->request()->getBody();

        // Validate data here

        $phonenumber = \ORM::for_table('phonenumber')->create();
        $phonenumber->set($body);
        if ($phonenumber->save() === true) {
            $data['phonenumber'] = $phonenumber->asArray();
            $output = array(
                'code' => 200,
                'data' => $data
            );
        echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

PUT /users/:id/phonenumbers/:pid

PUT route to update a user’s phonenumber.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// PUT route, for updating a user's phonenumber
$app->put('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumber = \ORM::forTable('phonenumber')
            ->findOne($phoneId);

        if ($phonenumber) {
            $data = $user->asArray();
            // Get POSTed data in body
            $body = $app->request()->getBody();

            // Validate data here

            $phonenumber->set($body);
            if ($phonenumber->save() === true) {
                $data['phonenumber'] = $phonenumber->asArray();
                $output = array(
                    'code' => 200,
                    'data' => $data
                );
            echo json_encode($output, JSON_PRETTY_PRINT);
                return;
            } else {
                throw new \Exception('Something went wrong.');
            }
        }
    }
   
    $app->notFound();
});

 

DELETE /users/:id/phonenumbers/:pid

And at the end, we define the route to delete an specific user’s phonenumber.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DELETE route, for delete a user's phonenumber
$app->delete('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $phonenumber = \ORM::forTable('phonenumber')
            ->findOne($phoneId);
        if ($phonenumber) {
            $phonenumber->delete();
            if ($phonenumber->save() === true) {
                $output = array('code' => 200, 'data' => array());
                echo json_encode($output, JSON_PRETTY_PRINT);
                return;
            } else {
                throw new \Exception('Something went wrong.');
            }
        }
    }
   
    $app->notFound();
});

 

Well, we finished defining all our end points to interect against our API. At this point we have a basic API working but still there are thing to do to make it more fully functional. For instance, what if we want to make our API secure? We should add authentication, and we’ll do it using really great Slim’s functionality Middlewares. Also, we can ensure all our requests/responses are being talking JSON and/or add some rate limit to don’t allow overwhelming our API.

In the next and last post we will cover all this. See you soon! :D.

Build simple REST API with PHP – Part 1

A web API (Application Programming Interface) is a set of programming instructions to access software applications and consume and share data between them. Developing your own API you can benefit both you and your users and for that we will write about how to build a simple REST API with PHP.

To build our API we will use Slim micro framework. As they mention in their site

Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.

As we said above an API is an Application Programming Interface for what it should be friendly, simple and easy to use. To help other developers to use our API we should consider writing a good documentation about it, or at least, a very explanatory README file. Such documentation need to be a summary of the service’s scope and the list of methods and access points.

Following the key principles of REST, each resource is represented by a URL, where the action is the HTTP verb used to access it.

A full list of access points is listed below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
URL                               HTTP Verb    Operation
/api/users                        GET          Returns an array of users
/api/users/:id                    GET          Returns the user with id of :id
/api/users                        POST         Adds a new user and return it with its id
/api/users/:id                    PATCH        Partially updates the user with id of :id
/api/users/:id                    PUT          Updates the complete user with id of :id
/api/users/:id                    DELETE       Deletes the user with id of :id

/api/users/:id/phonenumbers       GET          Returns the phonenumbers for the user with id of :id
/api/users/:id/phonenumbers/:pid  GET          Returns the phonenumber with id of :pid for the user with id of :id
/api/users/:id/phonenumbers       POST         Adds a new phonenumber for the user with id of :id
/api/users/:id/phonenumbers/:pid  PATCH        Partially updates the phonenumber with id of :pid for the user with id of :id
/api/users/:id/phonenumbers/:pid  PUT          Updates the phonenumber with id of :pid for the user with id of :id
/api/users/:id/phonenumbers/:pid  DELETE       Deletes the phonenumber with id of :pid for the user with id of :id

The above README is a good start point, but a more complete documentation would be better. You can consider some libraries to generate documentation like Google APIs Discovery Service, apiDoc or Swagger.

Setup

We define our composer.json with our project information and dependencies:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
    "name"
: "yourname/users-management",
    "description"
: "Simple RESTful API for users management",
    "license"
: "MIT",
    "authors"
: [
        {
            "name"
: "Your Name",
            "email"
: "you@yourdomain.com"
        }
    ],
    "require"
: {
        "slim/slim"
: "*",
        "slim/extras"
: "*",
        "slim/middleware"
: "*",
        "monolog/monolog"
: "*",
        "j4mie/paris"
: "*",
        "flynsarmy/slim-monolog"
: "*"
    },
    "archive"
: {
        "exclude"
: ["vendor", ".DS_Store", "*.log"]
    },
    "autoload"
: {
        "psr-0"
: {
            "API"
: "lib/"
        }
    }
}

As we can see in our composer.json, along with Slim we’re using Paris (lightweight Active Record implementation on top of Idiorm) to access the database layer and Monolog for logging.

Bootstrap our API

The bootstrap.php file is in charge to autoload classes and load configurations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Init application mode
if (empty($_ENV['SLIM_MODE'])) {
    $_ENV['SLIM_MODE'] = (getenv('SLIM_MODE'))? getenv('SLIM_MODE') : 'dev';
}

// Init and load configuration
$config = array();
$configFile = dirname(__FILE__) . '/config/config.'. $_ENV['SLIM_MODE'] . '.php';

if (is_readable($configFile)) {
    require_once $configFile;
} else {
    require_once dirname(__FILE__) . '/config/config.default.php';
}

// Create App
$app = new API\Application($config['app']);

// Get log writer
$log = $app->getLog();

// Init database
try {
    if (!empty($config['db'])) {
        \ORM::configure($config['db']['dsn']);
        if (!empty($config['db']['username'])
            && !empty($config['db']['password'])) {
            \ORM::configure('username', $config['db']['username']);
            \ORM::configure('password', $config['db']['password']);
        }
    }
} catch (\PDOException $e) {
    $log->error($e->getMessage());
}

// Add some Middlewares here (we discuss about this later)

First, we get the environment we are using (could be dev, staging, prod or whatever you want and define) and then we load the environment related configuration, it there is not, we load default configuration. Then we create the application which as you can see it’s an Application class which extends the basic Slim class; and at the end we try to connect to the database.

Later we will add some Middleware to our app, you can read about what a Middleware is here.

Front Controller

Our front controller index.php is the unique entry point to our API and it’s where the magic happens. Here we’ll define the HTTP verbs to interact with our API and be able to list/create/edit/delete our “Users” and “Phonenumbers“.

Here is our front controller where we’ll start writing our methods (HTTP verbs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
require_once dirname(__FILE__) . '/bootstrap.php';

// API group
$app->group('/api', function () use ($app, $log) {

    // Version group
    $app->group('/v1', function () use ($app, $log) {

        // GET route for all users
        $app->get('/users', function () use ($app, $log) {
            // Some code here
        });

        // GET route
        $app->get('/users/:id', function ($id) use ($app, $log) {
            // Some code here
        });

        // POST route, for creating
        $app->post('/users', function () use ($app, $log) {
            // Some code here
        });

        // PUT route, for updating
        $app->put('/users/:id', function ($id) use ($app, $log) {
            // Some code here
        });

        // DELETE route
        $app->delete('/users/:id', function ($id) use ($app, $log) {
            // Some code here
        });


        // GET route for all users's phonenumbers
        $app->get('/users/:id/phonenumbers', function ($id) use ($app, $log) {
            // Some code here
        });

        // GET route for specific user's phonenumber
        $app->get('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
            // Some code here
        });

        // POST route, for creating a new user's phonenumber
        $app->post('/users/:id/phonenumbers', function ($id) use ($app, $log) {
            // Some code here
        });

        // PUT route, for updating a user's phonenumber
        $app->put('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
            // Some code here
        });

        // DELETE route, for delete a user's phonenumber
        $app->delete('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
            // Some code here
        });
    });
});

We’ve created two nested groups, /api and /v1, so we can easily adhere to the “versioning in the URL” best practice to versioning APIs.

So far we’ve installed needed libraries, bootstrapped the app and defined the routes for all the end points our API will have. In the next parts of this post series we will add the logic for every method we’ve defined and we’ll add and write some Middlewares to make a fully functional API.

Keep in touch! 😀

My favorite stack

As I posted in an earlier post Javascript is conquering the world and in this post I want to share what is my favorite stack for building web applications.

For the latest apps I’m working I really like the following stack:

Symfony for the backend, basically the administrator interface.
AngularJS for the frontend and UI.
A RESTful API to be consumed by the frontend.

As I’m working with Symfony for a long time I decided to build my backend and administrator based on this PHP framework. For that I use a bundle I’ve written nammed HasheadoAdminBundle, but also I’m playing and experimenting with this EasyAdminBundle written by Javier Eguiluz.

For the frontend I’m using AngularJS, a really cool framework to extend HTML and build rich web applications. There is really good documentation to learn Angular in its own website and it also has a great community.

To build the RESTful API I’m using great Symfony bundles like FOSRestBundle and the JMSSerializerBundle. Here there is a really cool post to see how to build a cool API.

Well, I hope this post help you.

Migrate ElasticSearch data from one server to another one

As part of the logging system in a big project I’m working for a long time ago we are using Graylog to storage the logs coming from the application as well as from devices and services.

As you may know Graylog uses ElasticSearch to store the data (logs) and MongoDB to store some metadata and configuration.

For these days we are facing the need to grow our stack and make our logging system more scalable, and also, with the release of Graylog 1.0.1 we decided to build a new and more powerful logging system. Said that, and keeping on mind that we have to maintain the already existing logs, we faced the problem to migrate those existing logs (data) into the new ElasticSeach instance.

After some research on existing tools to do that, we found Elasticsearch-Exporter.

To install Elasticsearch-Exporter:

1
2
3
user@unix:~$ npm install nomnom
user@unix:~$ npm install colors
user@unix:~$ npm install elasticsearch-exporter --production

Then to copy all indices from server A to server B:

1
user@unix:~$ node exporter.js -a serverA -b serverB

After letting it run for a while, we start seeing messages like:

1
Processed 4144396 of 10626656 entries (39%)

And that’s. After the process finished we have the existing data in the new Server B. Enjoy it! 😀

Hasheado Admin Bundle

This is the bundle I use for admin generator in Symfony application. Basically, it’s an extension of SonataAdminBundle, but with different UI and FOSUserBundle already integrated for user and authentication management.

You can find it in:
Packagist
Github

You can follow this documentation to install it and use it:

Step 1: Download the Bundle

NOTE: This bundle depends in the SonataAdminBundle, FOSUserBundle and in the SonataDoctrineORMAdminBundle.

In an already working Symfony installation edit your composer.json file and add this line:

1
2
3
require: {
    "hasheado/admin-bundle"
: "dev-master"
}

and then, open a command console, enter your project directory and execute the following command to download the latest stable version of this bundle:

1
user@unix:~$ composer update

This command requires you to have Composer installed globally, as explained in the installation chapter of the Composer documentation.

Step 2: Enable the bundle

To enable the bundle we need to add the following line in the app/AppKernel.php file of your project:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
// app/AppKernel.php

// ...
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            // ...

            // Add dependencies
            new Sonata\CoreBundle\SonataCoreBundle(),
            new Sonata\BlockBundle\SonataBlockBundle(),
            new Knp\Bundle\MenuBundle\KnpMenuBundle(),
            new Sonata\DoctrineORMAdminBundle\SonataDoctrineORMAdminBundle(),
            new FOS\UserBundle\FOSUserBundle(),
            new Sonata\UserBundle\SonataUserBundle('FOSUserBundle'),
            new Sonata\EasyExtendsBundle\SonataEasyExtendsBundle(),

            // Then add SonataAdminBundle
            new Sonata\AdminBundle\SonataAdminBundle(),
            // And HasheadoAdminBundle
            new Hasheado\AdminBundle\HasheadoAdminBundle(),

            // ...
        );
    }

    // ...
}
?>

Step 3: Configuration

If you followed the installation instructions, HasheadoAdminBundle should be installed but inaccessible. You first need to configure it for your models before you can start using it.

Import the HasheadoAdminBundle’s config files:

1
2
3
4
5
6
7
8
9
10
11
12
13
# app/config/config.yml
imports
:
    - { resource
: @HasheadoAdminBundle/Resources/config/config.yml }

sonata_user
:
    class
:
        user
: pathToYourBundle\Entity\User
        group
: pathToYourBundle\Entity\Group

    fos_user
:
        user_class
: pathToYourBundle\Entity\User
    group
:
        group_class
: pathToYourBundle\Entity\Group

Of course, replace the path to your User and/or Group entities, and do not forget to make your classes extend the Sonata\UserBundle\Entity\BaseUser and the Sonata\UserBundle\Entity\BaseGroup classes.

Then you need to add the below security configuration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# app/config/security.yml
security
:
    role_hierarchy
:
        ROLE_ADMIN
: [ROLE_USER, ROLE_SONATA_ADMIN]
        ROLE_SUPER_ADMIN
: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
        SONATA
:
           - ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT # if you are using acl then this line must be commented

    providers
:
        fos_userbundle
:
           id
: fos_user.user_manager

    encoders
:
        FOS\UserBundle\Model\UserInterface
: sha512

    firewalls
:
       # Disabling the security for the web debug toolbar, the profiler and Assetic.
        dev
:
            pattern
: ^/(_(profiler|wdt)|css|images|js)/
            security
: false

    # ->; custom firewall for the admin area of the URL
    admin
:
        pattern
: /admin(.*)
        context
: user
        form_login
:
            provider
: fos_userbundle
            login_path
: /admin/login
            use_forward
: false
            check_path
: /admin/login_check
            failure_path
: null
        logout
:
            path
: /admin/logout
            target
: /admin
            anonymous
: true

    # -> end custom configuration

    acl
:
        connection
: default

    access_control
:
       # Admin login page needs to be access without credential
        - { path
: ^/admin/login$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path
: ^/admin/logout$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path
: ^/admin/login_check$, role: IS_AUTHENTICATED_ANONYMOUSLY }
        # Secured part of the site
        # This config requires being logged for the whole site and having the admin role for the admin part.
        # Change these rules to adapt them to your needs
        - { path
: ^/admin/, role: [ROLE_ADMIN, ROLE_SONATA_ADMIN] }
        - { path
: ^/.*, role: IS_AUTHENTICATED_ANONYMOUSLY }

At this point, the bundle is functional, but not quite ready yet.

You need to update the schema:

1
user@unix:~$ php app/console doctrine:schema:update --force

and create a new root user:

1
user@unix:~$ php app/console fos:user:create --super-admin

To be able to access HasheadoAdminBundle’s pages, you need to add its routes to your application’s routing file:

1
2
3
# app/config/routing.yml
admin
:
    resource
: '@HasheadoAdminBundle/Resources/config/routing.yml'

Finally, publish the assets:

1
user@unix:~$ php app/console assets:install

At this point you can already access the (empty) admin dashboard by visiting the url: http://yoursite.local/admin/dashboard. And, start adding your admins following SonataAdminBundle documentation.

PHP7 is almost here

The Last weekend in Miami, Florida, it took place the SunshinePHP conference where a lot of great members of the PHP community have gave great talks. I did not have the chance to be there, but I followed the talks through the social networks :D!

Rasmus Lerdorf (the PHP creator) was talking about PHP7, and here you can find his slides:

http://talks.php.net/sunshinephp15#

Some of the most important notes:

  • Engine improvements:
    • 100%+ performance gain on most real-world applications.
    • Lower memory usage.
    • Native thread local storage.
  • Removal of many deprecated features (Your PHP4 code will break!).
  • First Release Candidate scheduled for June 2015.

Also, in the slides there are a couple of chart that show you how much the engine is improved, sometimes more than HHVM. And, Rasmus encourages us to test this new PHP7 version and contribute.

Symfony best practices

A couple months ago the Symfony creator Fabien Potencier, alongside Ryan Weaver and Javier Eguiluz have created a handbook called “Symfony Best Practices“.

The reason of this handbook was due to the community has created an unofficial set of recommendations for developing Symfony applications. Unfortunately, a lot of these recommendations are unneeded for web applications.

Here, a summary of these best practices:

Projects creation

Use the Symfony installer.

You can read about it in an earlier post.

About bundles:

Create only one bundle called AppBundle for your application logic

Configuration

Define the infrastructure-related configuration options in the app/config/parameters.yml file.

Define all your application’s parameters in the app/config/parameters.yml.dist file.

Define the application behavior related configuration options in the app/config/config.yml file.

Organizing our Business Logic

Services: Name and Format

The name of your application’s services should be as short as possible, but unique enough that you can search your project for the service if you ever need to.

Use the YAML format to define your own services.

Don’t define parameters for the classes of your services.

Using a Persistence Layer:

Use annotations to define the mapping information of the Doctrine entities.

Controllers

Make your controller extend the FrameworkBundle base Controller and use annotations to configure routing, caching and security whenever possible.

Routing configuration:

Don’t use the @Template() annotation to configure the template used by the controller.

Use the ParamConverter trick to automatically query for Doctrine entities when it’s simple and convenient.

Templates

Use Twig templating format for your templates.

Store all your application’s templates in app/Resources/views/ directory.

Twig extensions:

Define your Twig extensions in the AppBundle/Twig/ directory and configure them using the app/config/services.yml file.

Forms

Define your forms as PHP classes.

Add buttons in the templates, not in the form classes or the controllers.

Internationalization

Use the XLIFF format for your translation files.

Store the translation files in the app/Resources/translations/ directory.

Always use keys for translations instead of content strings.

Security

Unless you have two legitimately different authentication systems and users (e.g. form login for the main site and a token system for your API only), we recommend having only one firewall entry with the anonymous key enabled.

Use the bcrypt encoder for encoding your users’ passwords.

Web Assets

Store your assets in the web/ directory.

Use Assetic to compile, combine and minimize web assets, unless you’re comfortable with frontend tools like GruntJS.

Tests

Define a functional test that at least checks if your application pages are successfully loading.

Hardcode the URLs used in the functional tests instead of using the URL generator.

Well, a great handbook to follow up, but of course, this are “best practices” and they are not a must.

PHP and Behavior Driven Desing

During this 2014 I was hearing and reading a lot about Behavior Driven Desing (BDD), which is a software development process based on Test Driven Design (TDD) combining ideas from Domain Driven Design (DDD) and Object Oriented Programming (OOP). For a full explanation about BDD click here.

In the tutsplus.com website we can find an in progress tutorial which explains how to use BDD in PHP using the Laravel PHP framework, Behat and PhpSpec. The tutotial is called “Laravel, BDD and You” and in this post I will be linking to every post in this serie.