Wednesday, April 1, 2015

Setting up our Silex Application

Setting up the application 

When I first started with Silex, it was very easy. Just a few files, a few routes, and then I was good to go. What I realized is that eventually my index.php page started to look like this:

error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
date_default_timezone_set('America/Phoenix');
// web/index.php
require_once __DIR__.'/vendor/autoload.php';


use Whoops\Provider\Silex\WhoopsServiceProvider;
use Symfony\Component\HttpFoundation\Request;
use Mailgun\Mailgun;

$app = new Silex\Application();
$app['debug']=true;
if ($app['debug']) {
    $app->register(new WhoopsServiceProvider());
}

$app->register(new Silex\Provider\SwiftmailerServiceProvider());
$app->register(new Silex\Provider\SessionServiceProvider());
$app['swiftmailer.options']=[
    'host'=>'mail.site.com',
    'port'=>'25',
    'username'=>'support@site.com',
    'password'=>'secret',
    'encryption'=>null,
    'auth_mode'=>null,
];

$app->register(new Silex\Provider\TwigServiceProvider(), [
    'twig.path' => [
        __DIR__.'/src/Api/Views/Emails',
    ],
]);


$app->get('/',function(){
    return'hello world';
});
$prefix = '/api';

$app->get($prefix.'auth/check', 'Api\\Auth::check');
$app->post($prefix.'auth/login','Api\\Auth::login');
$app->post($prefix.'auth/logout','Api\\Auth::logout');
$app->post($prefix.'contact/contact','Api\\Contact::us');
$app->post($prefix.'contact/password','Api\\Contact::password');
$app->get($prefix.'site/info','Api\\Site::info');
$app->post($prefix.'signup/individual','Api\\SignUp::individual')->after('Api\\Events\\Emails\\Welcome::individual');
$app->get($prefix.'signup/loginCheck','Api\\SignUp::checkLogin');
$app->get($prefix.'signup/userCheck','Api\\SignUp::checkUser');
$app->post($prefix.'signup/site','Api\\SignUp::site')->after('Api\\Events\\Emails\\Welcome::site');
$app->post($prefix.'user/updateProfile','Api\\User::updateProfile');
$app->post($prefix.'user/addApplication/{appId}','Api\\User::addApplication');
$app->post($prefix.'user/removeApplication/{appId}','Api\\User::removeApplication');
$app->post($prefix.'user/updateProfile','Api\\User::updateProfile');

$app->run();

This was a very simple API that I wrote, and we stopped production on it. What I started to notice was that my index.php was starting to get big. Routes started piling up. Application configuration, which was simple now, was getting more and more complex when I would add new service providers. Basically, it was becoming a cluster, and I knew there had to be a better way. I first looked at a few frameworks and the index.php page they used. Yii (not 2.0) looked like this:

$yii=dirname(__FILE__).'/yii/framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
require_once($yii);
date_default_timezone_set('America/Phoenix');
Yii::createWebApplication($config)->run();

Laravel looked like this:

/**
 * Laravel - A PHP Framework For Web Artisans
 *
 * @package  Laravel
 * @author   Taylor Otwell 
 */

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any our classes "manually". Feels great to relax.
|
*/

require __DIR__.'/../bootstrap/autoload.php';

/*
|--------------------------------------------------------------------------
| Turn On The Lights
|--------------------------------------------------------------------------
|
| We need to illuminate PHP development, so let's turn on the lights.
| This bootstraps the framework and gets it ready for use, then it
| will load up this application so that we can run it and send
| the responses back to the browser and delight these users.
|
*/

$app = require_once __DIR__.'/../bootstrap/start.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can simply call the run method,
| which will execute the request and send the response back to
| the client's browser allowing them to enjoy the creative
| and wonderful application we have whipped up for them.
|
*/

$app->run();

To set up my index.php page, I needed to do something like this, and break up all the clutter and organize my application better. To accomplish this, you can do a few different things. First option is to write a class and inject the Silex\Application, the other option is to extend the Silex\Application class. By doing either of these options, you can essentially reduce your index.php down to this:

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

$app = new MyApp\Application();
$app->bootstrap();
$app->run();

Very simple, very sleak, very easy to read. Broken down, all the index.php does is load the composer autoloader, creates the application, bootstraps the app (does the configuration, routes, etc), and then runs the application.

Folder Structure 

Before we go any further, I wanted to share my folder structure.

MySilexApp
----node_modules
----public
--------app
--------assets
--------dist
--------lib
--------.htaccess
--------index.php
----src
--------MyApp
------------Config
----------------Routing
----------------dev.json
----------------prod.json
----------------testing.json
------------Controllers
------------Middlewares
------------Repositories
------------Services
------------Views
----------------Main
--------------------index.twig
------------Application.php
----tests
--------MyApp
----vendor
----.bowerrc
----bower.json
----composer.json
----composer.lock
----gulpfile.js
----package.json
----phpunit.xml

I hope this all makes sense, but essentially my PHP application is inside of the /src/MyApp folder.  Tests are in the tests folder.  The Javascript application is in the public folder.  Composer packages in the vendor folder.  The reason I have the application structure set up this way just because it is simple.  It makes sense.  We can expose just the public folder to the user and hide the PHP application.  Tests are separated from our application, but will conform to the same folder structure as the application when tests are written.

Inside the application (src/MyApp) folder there are several sub-folders.

Config 

This is where your configuration files will go. Right now I have different config files for different environments. Inside the folder, there is a place to add the Route files, which we will go over later. The reason I added it here is because it is a flat file, no logic needed. The Routes files are simply files that specify the paths, controllers, and middlewares to be used.

Controllers 

This folder is just a folder full of controllers. Following best practices, your controllers should be slim and simple, so the main logic for your application will be done in a the other folders. 

Middlewares 

Name says it all. It just contains the middlewares that will be used by the various controllers. 

Repositories 

If you are using a database, this would be where you handle your data. If you read my previous posts, you know that I am using Soap right now to interact with an external API server. So the Repository folder houses all my connections and data retreival methods.

Services

This is a generic term for miscellaneous classes. One instance would be a class that handles data transformations (take a database object and return a smaller array or combines multiple arrays), or maybe a wrapper for another class/package you use. I have a wrapper for Zend/Soap/Client that is specific to my application. It just helps make the soap calls a little easier. I could potentially use it for another Soap server, but it is designed with the options hard coded for my application. If I wanted, I could move the options (Soap version, compression settings, WSDL url, etc) into a config file, but right now there is no need since I will most likely be moving to a database in the future and not additional soap servers. </rant>

Views

The folder that contains the twig files, or any templating file if you are using another templating system.

 One folder I haven't included, but will probably add in the future is a Validation folder. The folder would essentially handle validation for the application data submitted. When a request is received, the controller would load a Validator and data would be passed to it. Validated data would be returned and then, if valid, it could be submitted to the database.

The Application.php file

namespace MyApp;

use Whoops\Provider\Silex\WhoopsServiceProvider;
use Symfony\Component\HttpFoundation\Request;
use Silex\Provider;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\RouteCollection;

class Application extends \Silex\Application{
    public function bootstrap(){
        $this->loadProviders();
        $this->loadRoutes();
        $this->loadConfig();
        $this->connectMemcached();
    }
    private function loadProviders(){
        // load session service provider
        $this->register(new \Silex\Provider\SessionServiceProvider());
        $this['session.storage.handler']=null; // since using memcache as session handler, don't use silex to manage anything.

        // load twig service provider
        $this->register(new Provider\TwigServiceProvider(),['twig.path' => __DIR__.'/Views']);

        // the following providers are required by the WebProfiler
        $this->register(new Provider\HttpFragmentServiceProvider());
        $this->register(new Provider\ServiceControllerServiceProvider());
        $this->register(new Provider\UrlGeneratorServiceProvider());

        // get the application environment
        $env = getenv('APP_ENV') ?: 'prod';

        // load a configuration service provider and add configuration options
        $this->register(new \Igorw\Silex\ConfigServiceProvider(__DIR__."/Config/$env.json"));

        // load debug only providers (whoops)
        if ($this['debug']) {
            \Symfony\Component\Debug\Debug::enable(E_ALL, true);
            $this->register(new WhoopsServiceProvider());

            // level 2 debug or localDebug enable the WebProfiler
            if($this['localDebug']){
                $this->register($p = new Provider\WebProfilerServiceProvider(), [
                    'profiler.cache_dir' => __DIR__.'/../../cache/profiler',
                ]);
                $this->mount("_profiler", $p);
            }
            
        }
    }


    private function loadRoutes(){
        // use the Yaml File Loader to load up the routes in the routing folder
        $loader = new YamlFileLoader(new FileLocator(__DIR__ . '/Config/Routing'));
        $collection = $loader->load('routes.yml');

        // add the routes to the application
        $this['routes']->addCollection($collection);
    }
    private function loadConfig(){
        // the other options that you can set go here
        date_default_timezone_set('America/Phoenix');

        // add a simple middleware that looks for application/json requests and adds the paramters to the request object
        $this->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());
            }
        });
    }
    private function connectMemcached(){
        $this->register(new \SilexMemcache\MemcacheExtension(), [
            'memcache.library' => $this['memcacheLibrary'],
            'memcache.server' => [
                ['127.0.0.1', 11211]
            ],
        ]);
    }
}

What I have done is simply extend the Silex\Application and added a few more methods. Again, I could have written a wrapper instead, but felt this was a little easier at the time.

If you recall my index.php, it had the line "$app->bootstrap();" All that line does is loads all the service providers, routes, and configuration for the application.

The service providers are straight forward. The one thing I would like to point out is the application environment. One awesome thing you can do in Apache is set environment variables. In the vhosts file, you can add a line like this:

    DocumentRoot "path\to\htdocs"
    ServerName localhost
    SetEnv APP_ENV dev

On your production server, you can either leave that line out, or you can change the vhosts file to prod (for production). Then in PHP you can use the
getenv('APP_ENV')
to get the information that you set in the Apache file. You can do this in Nginx as well, but I don't have that code off hand. Bringing this full circle, I can have a different configuration that is automatically selected based on this environment variable and then I can use those options in my application. The ConfigServiceProvider is a 3rd party provider and will allow me to set configuration keys and values fairly easily. I can then access the variables like this:
$value = $app['key'];

Simple. In the configuration file I can specify debugging options. On a production server, you don't want to show Whoops error pages when there is a bug (hopefully there aren't any). You definitely don't want to display the webprofiler. On a local machine, you would want to see both of these things. Routes The routes are now being pulled from a file! How cool is that. Because Silex is using Symfony components (Routes), you can load routes using the RouteCollection provided by Symfony. I put the routes into the configuration folder in YAML files. Here is my routes.yml:

# config/routes.yml
home:
    path: /
    defaults: { _controller: MyApp\Controllers\IndexController::actionIndex }

hello:
    path: /hello/{name}
    defaults: { _controller: MyApp\Controllers\IndexController::actionHello }

auth:
    prefix: /auth
    resource: auth.yml

user:
    prefix: /user
    resource: user.yml

One thing to notice is that my routes have subroute resources (auth and user) that reference additional YAML files. The YAML file loader will load those routes as well.

Auth looks like this:

# config/auth.yml
auth.check:
    path: /check
    defaults: { _controller: MyApp\Controllers\AuthController::actionCheck}
    methods: [get]

auth.getLogin:
    path: /login
    defaults: { _controller: MyApp\Controllers\AuthController::actionGetLogin}
    methods: [get]

auth.postLogin:
    path: /login
    defaults: { _controller: MyApp\Controllers\AuthController::actionPostLogin}
    methods: [post]

auth.logout:
    path: /logout
    defaults: { _controller: MyApp\Controllers\AuthController::actionLogout}

And the User routes look like this:

# config/auth.yml
user.apps:
    path: /apps
    defaults: {_controller: MyApp\Controllers\UserController::actionGetApps}
    methods: [get]
    options: {_before_middlewares: [ MyApp\Middlewares\AuthMiddleware::beforeAuth ]}

Now to explain this. The routes.yml file has two defined routes: home and hello. Those are the route names and can be accessed that way anytime when doing redirects. The controller being used is in the defaults: object with the key "_controller". The two subroutes are auth and user. The auth routes are all named with an "auth." prefix and can again be accessed that way as well. The routes are all prefixed with "/auth" so auth.check has a path like "/auth/check". The other thing in the auth.yml file is that you can specify route methods. If you leave it blank, that route can be called via any method. In the auth file I have two separate routes, one for posting to /auth/login and another for getting the /auth/login. The call different controller methods and make it easier than using
$_SERVER['REQUEST_METHOD']
to see how things are being accessed. The user.yml routes have one additional parameter: options. Here you can specify middlewares that the route will use. The middleware object keys are "_before_middlewares" and "_after_middlewares", and both are arrays, so you can pass several middlewares to a single route.

 The routes are all converted to an array object from the YAML files, and are then loaded into the application using a RouteCollection. You can have multiple RouteCollections, and adding them will continue pushing the routes onto the $app['routes'], not overwriting them.

Wrap up 

I think this is long enough for now. We covered your Application.php setup and routing for your application. We have packed in a ton of routes using YAML files and the RouteCollection. We have configured our application for a specific development environment. I think tomorrow I am going to talk about caching and benefits using cache over a file system.

No comments:

Post a Comment