Création d'une API REST avec Silex

Création d'une API REST avec Silex

Publié le 17/12/2015

On va créer une application Silex avec une table User. L'application va disposer d'un API REST pour l'entité User.

Elle disposera des méthodes suivantes :

  • - affiche tous
  • - affiche un
  • - créer
  • - modifier
  • - supprimer

Attention, cet exemple est une implémentation à minima et n'est donc pas sécurisé : Ne pas utiliser en prod.

1 - Création de l'application Silex

On commence par créer un vhost et une base de données. Puis on va créer un dossier pour notre application web et dans ce dernier on va ajouter un fichier composer.json.

Je passe sur les étapes de création de vhost et de db (cf. les autres tutoriels du site). On passe directement à la création du fichier composer.json :

nano composer.json


{
    "require": {
        "silex/silex": "1.3.*",
        "doctrine/dbal": "2.5.*"
    },
    "autoload" : {
        "psr-4": {"SilexApi\\": "src"}
    }
}

Installation de composer.phar et des vendors :


curl -s https://getcomposer.org/installer | php
php compser.phar install

On va ensuite créer les répertoires qui vont structurer notre app :

mkdir app app/config src web

Création de la base de données :


mkdir db
nano db/database.sql

create database if not exists silex_api character set utf8 collate utf8_unicode_ci; use silex_api; drop table if exists user; create table user ( id integer not null primary key auto_increment, firstname varchar(255) not null, lastname varchar(255) not null ) engine=innodb character set utf8 collate utf8_unicode_ci; insert into user values (1, 'Jean', 'Duchmol'); insert into user values (2, 'Sophie', 'Tartempion');

Exécution du script de création de db :

mysql -u root -p silex_api < db/database.sql

2 - Création du front controller

On crée le front controller dans le dossier web :

nano web/index.php


<?php

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

$app = new Silex\Application();

require __DIR__.'/../app/config/dev.php';
require __DIR__.'/../app/app.php';
require __DIR__.'/../app/routes.php';

$app->run();

Ajout du fichier .htaccess pour l'url rewriting :

nano web/.htaccess


DirectoryIndex index.php


    RewriteEngine On

    RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$
    RewriteRule ^(.*) - [E=BASE:%1]

    RewriteCond %{HTTP:Authorization} .
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

    RewriteCond %{ENV:REDIRECT_STATUS} ^$
    RewriteRule ^index\.php(/(.*)|$) %{ENV:BASE}/$2 [R=301,L]

    RewriteCond %{REQUEST_FILENAME} -f
    RewriteRule .? - [L]

    RewriteRule .? %{ENV:BASE}/index.php [L]



    
        RedirectMatch 302 ^/$ /index.php/
    


3 - Création du noyau de l'app

nano app/app.php


<?php

use Symfony\Component\Debug\ErrorHandler;
use Symfony\Component\Debug\ExceptionHandler;
use Symfony\Component\HttpFoundation\Request;

ErrorHandler::register();
ExceptionHandler::register();

$app->register(new Silex\Provider\DoctrineServiceProvider());

$app['dao.user'] = $app->share(function ($app) {
	return new SilexApi\UserDao($app['db']);
});

// Register JSON data decoder for JSON requests
$app->before(function (Request $request) {
	if (0 === strpos($request->headers->get('Content-Type'), 'application/json')) {
		$data = json_decode($request->getContent(), true);
		$request->request->replace(is_array($data) ? $data : array());
	}
});

4 - Création des fichiers de configuration

nano app/config/dev.php


<?php

// Doctrine (db)
$app['db.options'] = array(
	'driver'   => 'pdo_mysql',
	'charset'  => 'utf8',
	'host'     => '127.0.0.1',  // Mandatory for PHPUnit testing
	'port'     => '3306',
	'dbname'   => 'SilexApi',
	'user'     => 'user',
	'password' => 'pwd',
);

// enable the debug mode
$app['debug'] = true;

nano app/config/prod.php


<?php

// Doctrine (db)
$app['db.options'] = array(
	'driver' => 'pdo_mysql',
	'charset' => 'utf8',
	'host' => 'localhost',
	'port' => '3306',
	'dbname' => 'SilexApi',
	'user' => 'user',
	'password' => 'pwd'
);

5 - Création du model pour la table User

Notre table user va comporter les champs : "id", "firstname" et "lastname".

On va d'abord créer un model pour cette table :

nano src/User.php


<?php

namespace SilexApi;

class User
{
	/**
	 * @var integer
	 */
	private $id;

	/**
	 * @var string
	 */
	private $firstname;

	/**
	 * @var string
	 */
	private $lastname;

	
	/**
	 * @return int
	 */
	public function getId() {
		return $this->id;
	}

	/**
	 * @param int $id
	 */
	public function setId( $id ) {
		$this->id = $id;
	}

	/**
	 * @return string
	 */
	public function getFirstname() {
		return $this->firstname;
	}

	/**
	 * @param string $firstname
	 */
	public function setFirstname( $firstname ) {
		$this->firstname = $firstname;
	}

	/**
	 * @return string
	 */
	public function getLastname() {
		return $this->lastname;
	}

	/**
	 * @param string $lastname
	 */
	public function setLastname( $lastname ) {
		$this->lastname = $lastname;
	}
}

On crée ensuite une classe pour l'appel des données :

nano src/UserDAO.php


<?php

namespace SilexApi;

use Doctrine\DBAL\Connection;

class UserDao
{
	private $db;

	public function __construct(Connection $db)
	{
		$this->db = $db;
	}

	protected function getDb()
	{
		return $this->db;
	}

	public function findAll()
	{
		$sql = "SELECT * FROM user";
		$result = $this->getDb()->fetchAll($sql);

		$entities = array();
		foreach ( $result as $row ) {
			$id = $row['id'];
			$entities[$id] = $this->buildDomainObjects($row);
		}

		return $entities;
	}

	public function find($id)
	{
		$sql = "SELECT * FROM user WHERE id=?";
		$row = $this->getDb()->fetchAssoc($sql, array($id));

		if ($row) {
			return $this->buildDomainObjects($row);
		} else {
			throw new \Exception("No user matching id ".$id);
		}
	}

	public function save(User $user)
	{
		$userData = array(
			'firstname' => $user->getFirstname(),
			'lastname' => $user->getLastname()
		);

		// TODO CHECK
		if ($user->getId()) {
			$this->getDb()->update('user', $userData, array('id' => $user->getId()));
		} else {
			$this->getDb()->insert('user', $userData);
			$id = $this->getDb()->lastInsertId();
			$user->setId($id);
		}
	}

	public function delete($id)
	{
		$this->getDb()->delete('user', array('id' => $id));
	}

	protected function buildDomainObjects($row)
	{
		$user = new User();
		$user->setId($row['id']);
		$user->setFirstname($row['firstname']);
		$user->setLastname($row['lastname']);

		return $user;
	}
}

6 - Créations des routes/controllers

nano app/routes.php


<?php

use Symfony\Component\HttpFoundation\Request;
use SilexApi\User;

// Get all users
$app->get('/api/users', function () use ($app) {

	$users = $app['dao.user']->findAll();
	$responseData = array();
	foreach ($users as $user) {
		$responseData[] = array(
			'id' => $user->getId(),
			'firstname' => $user->getFirstname(),
			'lastname' => $user->getLastName()
		);
	}

	return $app->json($responseData);
})->bind('api_users');

// Get on user
$app->get('/api/user/{id}', function ($id, Request $request) use ($app) {
	$user = $app['dao.user']->find($id);
	if (!isset($user)) {
		$app->abort(404, 'User not exist');
	}

	$responseData = array(
		'id' => $user->getId(),
		'firstname' => $user->getFirstname(),
		'lastname' => $user->getFirstname()
	);

	return $app->json($responseData);
})->bind('api_user');

// Create user
$app->post('/api/user/create', function (Request $request) use ($app) {
	if (!$request->request->has('firstname')) {
		return $app->json('Missing parameter: firstname', 400);
	}
	if (!$request->request->has('lastname')) {
		return $app->json('Missing parameter: lastname', 400);
	}

	$user = new User();
	$user->setFirstname($request->request->get('firstname'));
	$user->setLastname($request->request->get('lastname'));
	$app['dao.user']->save($user);

	$responseData = array(
		'id' => $user->getId(),
		'firstname' => $user->getFirstname(),
		'lastname' => $user->getLastname()
	);

	return $app->json($responseData, 201);
})->bind('api_user_add');

// Delete user
$app->delete('/api/user/delete/{id}', function ($id, Request $request) use ($app) {
	$app['dao.user']->delete($id);

	return $app->json('No content', 204);
})->bind('api_user_delete');

// Update user
$app->put('/api/user/update/{id}', function ($id, Request $request) use ($app) {
	$user = $app['dao.user']->find($id);

	$user->setFirstname($request->request->get('firstname'));
	$user->setlastname($request->request->get('lastname'));
	$app['dao.user']->save($user);

	$responseData = array(
		'id' => $user->getId(),
		'firstname' => $user->getFirstname(),
		'lastname' => $user->getLastname()
	);

	return $app->json($responseData, 202);
})->bind('api_user_update');

7 - Test de l'API

On va tester notre API avec le plug-in Firefox RESTClient (https://addons.mozilla.org/fr/firefox/addon/restclient/).

Dans le plugin, allez dans le menu "Headers" et choisissez l'option "Custom Header" et ajouter les paramètres suivants :

  • name : Content-Type
  • Value : application/json

Sélectionner le verbe en fonction des requêtes :

  • - GET > findAll et find
  • - POST > create
  • - PUT > update
  • - DELETE > delete