David Makin's avatar

Creating a simple REST application with Silex part 3.

In part 1 you installed Silex and setup 2 routes, / and /{stockcode}. In part 2 you added a POST and a DELETE route. In part 3 we are going to assume a few months have passed and you now have tens, maybe hundreds, of routes.

The problem with having many routes is that we end up with a really big and unwieldy index.php file. Finding the correct route to fix can take a while. I will be showing you two possible way to manage your routes.

After adding our new routes into index.php we end up with a file somewhat like this.

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
<?php

$app->get('/', function() use ($toys) {
    // body here
});

$app->get('/{stockcode}', function (Silex\Application $app, $stockcode) use ($toys) {
    // body here
});

$app->post('/', function (Silex\Application $app, Symfony\Component\HttpFoundation\Request $request) {
    // body here
});

$app->delete('/{stockcode}', function (Silex\Application $app, Symfony\Component\HttpFoundation\Request $request, $stockcode) {
    // body here
});

$app->match('/{stockcode}/image', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
})->method('GET|POST');

$app->get('/giftcodes', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
});

$app->get('/giftcodes/{giftcode}', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
});

$app->post('/giftcodes', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
});

$app->get('/stats', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
});

$app->get('/stats/{stockcode}', function (Silex\Application $app, $stockcode) use ($toys) {     
    // body here
});

//...
//... Other made up routes here
//...

You can see from this small example that finding a particular route is not easy. Imagine the example with the body code for the routes as well. Currently no IDEs can cope with routes and you will not get a nice list of route names like you do for functions.

Solution 1 - Group and separate

The easiest solution is to group up your routes and move them into separate files.

Lets split our code into 3 files, stockcode.php, giftcodes.php and stats.php. Into stockcode.php we will move the routes

    GET /
    GET /{stockcode}
    POST /
    DELETE /
    GE|POST /{stockcode}/image

giftcodes.php will have

    GET /giftcodes
    GET /giftcodes/{giftcode}
    POST /giftcodes

stats.php will have

    GET /stats
    GET /stats/{stockcode}

We can now simplify our index.php down to

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

$filename = __DIR__.preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']);
if (php_sapi_name() === 'cli-server' && is_file($filename)) {
    return false;
}

$app = new Silex\Application();
// Please set to false in a production environment
$app['debug'] = true;

$toys = array(
    // ...
);

require(__DIR__ . 'stockcode.php');
require(__DIR__ . 'giftcodes.php');
require(__DIR__ . 'stats.php');

$app->run();

Now when we want to update a stockcode route we know to look in stockcode.php. Much simpler and hopefully easier to maintain.

Solution 2 - use Providers

As with solution 1 we are going to separate our code into 3 files, broken out along the same lines. For this we need to do more work than just a simple copy and paste. I will show you how to do this for the stockcode routes.

Toyshop\StockcodeController.php

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?php
namespace Toyshop;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Silex\Application;
use Silex\ControllerProviderInterface;

/**
 * The routes used for stockcode.
 *
 * @package toyshop
 */
class StockcodeController implements ControllerProviderInterface
{
    private $toys = array(
        '00001'=> array(
            'name' => 'Racing Car',
            'quantity' => '53',
            'description' => '...',
            'image' => 'racing_car.jpg',
        ),
        '00002' => array(
            'name' => 'Raspberry Pi',
            'quantity' => '13',
            'description' => '...',
            'image' => 'raspberry_pi.jpg',
        ),
    );    
    
    /**
     * Connect function is used by Silex to mount the controller to the application.
     *
     * Please list all routes inside here.
     *
     * @param Application $app Silex Application Object.
     *
     * @return Response Silex Response Object.
     */
    public function connect(Application $app)
    {
        /**
         * @var \Silex\ControllerCollection $factory
         */
        $factory = $app['controllers_factory'];

        $factory->get(
            '/',
            'Toyshop\StockcodeController::getAll'
        );     
        
        $factory->get(
            '/{stockcode}',
            'Toyshop\StockcodeController::getStockcode'
        );        

        $factory->delete(
            '/',
            'Toyshop\StockcodeController::deleteStockcode'
        );        

        // ... Other routes     

        return $factory;
    }

    /**
     * Get all the stockcodes.
     *
     * @param Application $app       The silex app.
     *
     * @return string
     */
    public function getAll(Application $app)
    {
        return json_encode($this->toys);
    }

    /**
     * Get a stockcode.
     *
     * @param Application $app       The silex app.
     * @param string      $stockcode The stockcode.
     *
     * @return string
     */
    public function getStockcode(Application $app, $stockcode)
    {
        if (!isset($this->toys[$stockcode])) {
            $app->abort(404, "Stockcode {$stockcode} does not exist.");
        }
        return json_encode($this->toys[$stockcode]);
    }


    /**
     * Delete a stockcode.
     *
     * @param Application $app       The silex app.
     * @param string      $stockcode The stockcode.
     *
     * @return string
     */
    public function deleteStockcode(Application $app, $stockcode)
    {
        if (delete_toy($stockcode)) {
            // The delete went ok and we can now return a no content value
            // HTTP_NO_CONTENT = 204
            $responseCode = Response::HTTP_NO_CONTENT;
        } else {
            // Something went wrong
            $response_code = Response::HTTP_INTERNAL_SERVER_ERROR;
        }
    
        return new Response('', $responseCode);
    }    
    
    // Other functions here
    // ...
    // ...
}

The important function in StockcodeController.php is connect() as this is now responsible for the routing. We are making use of a controller_factory which you must return from connect().

If you look at the entry for the route GET /{stockcode} you will notice that I have changed it from an anonymous function into a call to a function inside the class.

<?php
        $factory->get(
            '/{stockcode}',
            'Toyshop\StockcodeController::getStockcode'
        );  

You could keep all the routs as anonymous but moving them into their own functions helps you maintain your ever growing number of routes. A bonus from this is that your IDE will now show then as functions and if you name them in helpful ways you will easily be able to figure out which route they are associated with.

In the index.php file we will use the mount() command instead of require().

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

$filename = __DIR__.preg_replace('#(\?.*)$#', '', $_SERVER['REQUEST_URI']);
if (php_sapi_name() === 'cli-server' && is_file($filename)) {
    return false;
}

$app = new Silex\Application();
// Please set to false in a production environment
$app['debug'] = true;

$toys = array(
    // ...
);

$app->mount('/stockcode', new Toyshop\StockcodeController());
$app->mount('/giftcodes', new Toyshop\GiftcodeController());
$app->mount('/stats', new Toyshop\StatsController());

$app->run();

Which is best?

Solution 1 is only a short term fix and only really moves the problem into other files whereas solution 2 fixes it in a nice way and allows you to share your providers with other applications. Go for solution 2. It might be more work for you to start with but it really does help you out a lot more.

Further reading

Hopefully I have helped you to think about how to arrange you routes but please take the time the read the Organizing Controllers documentation on the Silex webpage for more information about how this works.

part 1
part 2

Organizing Controllers - Silex Documentation Creating a provider - Silex Documentation

comments powered by Disqus