Serverside Webscripting [JLW384]

04.silex.part2

It looks like you are viewing these slides on GitHub. Please clone the source repository in order to get the included PHP examples to work.

Dependency Injection

He's good, alright. But he's no Clem Johnson. And Johnson played back in the days before steroid injections were mandatory.

Dependency Injection

  • Design pattern in which you inject dependencies, instead of creating them in the class requiring them.
  • Injection possible via
    • The constructor (most used)
    • A (public) property or mutator function (protected or private property)
  • DI results in better code
    • Easier to test
    • Inversion of Control (see further)

Dependency Injection Example (1/2)

  • Without DI:
    <?php
    
    class Router {
    
    	protected $request;
    	protected $response;
    
    	public function __construct($path) {
    		$this->request = new Request();
    		$this->response = new Response();
    		$this->path = $path;
    		// …
    	}
    
    }
    
    $router = new Router('/hello');
    var_dump($router);

Dependency Injection Example (2/2)

  • With DI:
    <?php
    
    class Router {
    
    	protected $request;
    	protected $response;
    
    	public function __construct(Request $request, Response $response, $path) {
    		$this->request = $request;
    		$this->response = $response;
    		$this->path = $path;
    	}
    
    }
    
    $request = new Request();
    $reponse = new Response();
    $router = new Router($request, $response, '/hello');
    var_dump($router);
→ We have injected the dependencies via the constructor

Inversion of Control

  • With DI we have inverted the control of dependencies from the one being called (Router) to the one calling (you).
    • This is called Inversion of Control
  • → Dependency Injection affords Inversion of Control
  • But … why?
    • You have control over the dependecies: if you rather use SwiftMailer instead of PHPMailer, just inject that one onto the class requiring it
      • Note: the APIs of those libraries must be the same!
      • To afford this, a few PSRs exist; e.g. PSR-3

Dependency Injection Container

  • DiC = a container object …
    • … on which you store all dependencies (object registry)
    • … which can create dependencies (object factory)
  • Very (very) simple DiC example:
    class Container {
    
    	protected $dependencies = [];
    
    	public function set($name, $val) {
    		$this->dependencies[$name] = $val;
    	}
    
    	public function get($name) {
    		return isset($this->dependencies[$name]) ? (is_callable($this->dependencies[$name]) ? $this->dependencies[$name]() : $this->dependencies[$name]) : null;
    	}
    
    }

DiC Usage

  • Using the DiC from the previous slide:
    // Router Class
    class Router {
    
    	protected $request;
    	protected $response;
    
    	public function __construct(Request $request, Response $response, $path) {
    		$this->request = $request;
    		$this->response = $response;
    		$this->path = $path;
    	}
    
    }
    
    // Create DiC
    $container = new Container();
    
    // Tell DiC how to create dependencies
    $container->set('request', function() {
    	return new Request();
    });
    $container->set('response', function() {
    	return new Response();
    });
    
    // Create a router, injecting the dependencies
    $router = new Router($container->get('request'), $container->get('response'), '/hello');

DiC Features

  • Object Instantiation
    • Act as a factory to create new objects
  • Auto-Wiring
    • Say A requires A and B requires C
    • If you request an A from the DiC, it should create all required dependencies (B and C)
  • → Our DiC here only does Object Instantiation.

Service Locator

  • Codewise exactly the same as DiC
    class ServiceLocator {
    
    	protected $dependencies = [];
    
    	public function set($name, $val) {
    		$this->dependencies[$name] = $val;
    	}
    
    	public function get($name) {
    		return isset($this->dependencies[$name]) ? (is_callable($this->dependencies[$name]) ? $this->dependencies[$name]() : $this->dependencies[$name]) : null;
    	}
    
    }
  • Consumption/usage is different:
    • Don't inject dependencies one by one but injec the entire container
    • Implementation will ask container for the dependencies it needs

SL Usage

  • Using the SL from the previous slide:
    // Router Class
    class Router {
    
    	protected $request;
    	protected $response;
    
    	public function __construct(ServiceLocator $sl, $path) {
    		$this->request = $sl->get('request');
    		$this->response = $sl->get('response');
    		$this->path = $path;
    	}
    
    }
    
    // Create SL
    $sl = new ServiceLocator();
    
    // Tell DiC how to create dependencies
    $sl->set('request', function() {
    	return new Request();
    });
    $sl->set('response', function() {
    	return new Response();
    });
    
    // Create a router, injecting the dependencies
    $router = new Router($sl, '/hello');
→ The Router has become container aware

DiC vs. SL

  • Push vs. Pull
  • Which is best?

Pimple Deepdive

I'll make you cry, buddy. You're a pimple on society's ass and you'll never amount to anything.

Pimple

  • As said before: Silex is built on top of Pimple ~1.0
    <?php
    
    namespace Silex;
    …
    class Application extends \Pimple implements … {
    	…
    }
  • Pimple is a Dependency Injection Container
    • Supports defining:
      • Parameters
      • Objects
      • Shared objects (cfr. Singleton)
Note: Silex uses Pimple ~1.0. It differs a lot from the latest Pimple version (3.x)

Creating a Pimple

  • Just create a new instance
    <?php
    
    require_once '/path/to/pimple.php';
    
    $container = new Pimple();
  • As Silex\Application extends \Pimple, our $app also is an instance of (a special type of) \Pimple
    <?php
    
    …
    
    $app = new Silex\Application();

Parameters

  • Defining a parameter:
    $app['some_parameter'] = 'value';
  • Retrieving a parameter:
    echo $app['some_parameter']; // value

Service Definitions

A service is an object that does something as part of a larger system. […] Almost any global object can be a service.
  • Defining a service:
    $app['some_service'] = function () {
    	return new Whatever();
    };
  • Retrieving a service:
    $service = $app['some_service'];
    • Every time you call $app['some_service'], a new instance of the service is created.

Shared Services

By default, each time you get a service, Pimple returns a new instance of it. If you want the same instance to be returned for all calls, use a shared service.
  • Defining a shared service:
    $app['some_service'] = $app->share(function () {
    	return new Whatever();
    });
  • Retrieving a shared service:
    $service = $app['some_service'];
    • On first invocation the service is created.
    • Subsequent requests will get the existing instance (cfr. Singleton)

Practical Example (1)

  • Facebook PHP SDK standalone example:
    require 'facebook-php-sdk/src/facebook.php';
    
    $facebook = new \Facebook(array(
      'appId'  => 'YOUR_APP_ID',
      'secret' => 'YOUR_APP_SECRET',
    ));
    
    // Get User ID
    $user = $facebook->getUser();
  • → Let's inject the Facebook object as a dependency of our Silex $app.

Practical Example (2)

  • Dependency Injection is done in bootstrap.php
    $app['facebook.app_id'] = 'YOUR_APP_ID';
    $app['facebook.secret'] = 'YOUR_APP_SECRET';
    
    $app['facebook'] = $app->share(function($app) {
    	return new \Facebook(array(
    		'appId' => $app['facebook.app_id'],
    		'secret' => $app['facebook.secret']
    	));
    });
  • Usage:
    $app->get('/', function(Silex\Application $app) {
    	$user = $app['facebook']->getUser();
    	return $app->json($user);
    });

Services in Silex

Final boarding call for Flight 406, non-stop service to pain!

The need for structure

  • As per usual, we like our code clean
    1. Don't overload app.php with routing logic
    2. Don't overload bootstrap.php with dependency injections
  • Solutions
    1. ControllerProviders (@see 03.silex.part1)
    2. ServiceProviders

ServiceProvider (1)

From the Silex Docs:

This is very straight forward, just create a new class that implements two methods. In the register() method, you can define services on the application which then may make use of other services and parameters. In the boot() method, you can configure the application, just before it handles a request.

ServiceProvider (2)

  • Cfr. ControllerProvider but different interface/methods
    namespace MyNamespace\Provider\Service;
    
    use Silex\Application;
    use Silex\ServiceProviderInterface;
    
    class WhateverServiceProvider implements ServiceProviderInterface {
    
    	public function register(Application $app) {
    		// Store the service on Pimple
    		$app['whatever'] = >share(function() {
    			return new Whatever();
    		}
    	}
    
    	public function boot(Application $app) { … }
    
    }
  • Place your class in /src/MyNamespace/Provider/Service
  • Register your class with Silex' $app using
    $app->register(new MyNamespace\Provider\Service\WhateverServiceProvider(), []);

Revised Example (1/2)

  1. Create the service (FacebookServiceProvider.php)
    <?php
    
    namespace Ikdoeict\Provider\Service;
    
    use Silex\Application;
    use Silex\ServiceProviderInterface;
    
    class FacebookServiceProvider implements ServiceProviderInterface {
    
    	public function register(Application $app) {
    		$app['facebook'] = $app->share(function() use ($app) {
    			return new \Facebook(array(
    				'appId' => $app['facebook.app_id'],
    				'secret' => $app['facebook.secret']
    			));
    		});
    	}
    
    	public function boot(Application $app) {}
    
    }

Revised Example (2/2)

  1. Register the service (bootstrap.php)
    $app->register(new Ikdoeict\Provider\Service\FacebookServerProvider(), array(
    	'facebook.app_id' => 'YOUR_APP_ID',
    	'facebook.secret' => 'YOUR_APP_SECRET'
    ));
  2. Use the service (app.php)
    $app->get('/', function(Silex\Application $app) {
    	$user = $app['facebook']->getUser();
    	return $app->json($user);
    });

More examples

Built-in Service Provider:
TwigServiceProvider

TwigServiceProvider

  • TwigServiceProvider provides Twig integration

Inclusion & Usage

  • Require Twig via Composer
    $ composer require twig/twig:~1.8
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\TwigServiceProvider(), array(
    	'twig.path' => __DIR__ . '/views',
    ));
  • Usage
    $app->get('/hello/{name}', function ($name) use ($app) {
    	return $app['twig']->render('hello.twig', array(
    		'name' => $name,
    	));
    });

Example

Let's take a look at assets/ws2-sws-fiddles-silex/05.tweets-twig/

Example Recap

  • Place your templates in /src/views/
    • Use subfolders per entity
      • /src/views/errors/: 404.twig
      • /src/views/tweets/: overview.twig & detail.twig
      • /src/views: layout.twig
  • In PHP use $app['twig']
  • The serviceprovider injects some vars into templates
    • e.g. {{ app.request.baseUrl }}

Built-in Service Provider:
SwiftServiceProvider

SwiftServiceProvider

  • SwiftServiceProvider provides SwiftMailer integration

Inclusion

  • Require SwiftMailer via Composer
    $ composer require swiftmailer/swiftmailer:~5.0
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\SwiftmailerServiceProvider(), array(
    	'swiftmailer.options' => array(
    		'host' => 'host',
    		'port' => '25',
    		'username' => 'username',
    		'password' => 'password',
    		'encryption' => null,
    		'auth_mode' => null
    	)
    ));

Usage

  • Usage
    $app->post('/feedback', function () use ($app) {
    	$request = $app['request'];
    
    	$message = \Swift_Message::newInstance()
    		->setSubject('[YourSite] Feedback')
    		->setFrom(array('noreply@yoursite.com'))
    		->setTo(array('feedback@yoursite.com'))
    		->setBody($request->get('message'));
    
    	$app['mailer']->send($message);
    
    	return new Response('Thank you for your feedback!', 201);
    });

Built-in Service Provider:
DoctrineServiceProvider

DoctrineServiceProvider

  • DoctrineServiceProvider provides Doctrine DBAL integration

Inclusion

  • Require Doctrine DBAL using Composer
    $ composer require doctrine/dbal:~2.2
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\DoctrineServiceProvider(), array(
    	'db.options' => array(
    		'driver' => 'pdo_mysql',
    		'host' => 'localhost',
    		'dbname' => 'todolist',
    		'user' => 'root',
    		'password' => 'Azerty123',
    		'charset' => 'utf8mb4'
    	)
    ));

Usage

  • Usage
    $app->get('/blog/{id}', function ($id) use ($app) {
    	$sql = 'SELECT * FROM posts WHERE id = ?';
    	$post = $app['db']->fetchAssoc($sql, array($id));
    
    	return  '<h1>' . $app->escape($post['title']) . '</h1>'.
    			'<p>' . $app->escape($post['body']) . '</p>';
    })->assert('id', '\d+');

Example

Let's take a look at assets/ws2-sws-fiddles-silex/06.doctrine/

(Be sure to install the example properly. @see the included README.md)

Recognizing a pattern …

Uh oh, I think we're going in circles. I recognize that pattern of striations on that gypsum formation.

What we already have

  • Front Controller & Routing
  • Controllers per entity
  • Views (templates) per entity
  • Data-access entangled in our controller

→ If we can split out our data access logic,
we can strive towards MVC

MVC

  • Short for Model-View-Controller
  • Architectural pattern describing how to build your app
  • In a web-context:
    • The model represents data
    • The view visually represents the model
    • The controller is the app logic: decides what the input was, and orchestrates the proper models/views

MVC

Model View Controller
MVC Schematic (ref)

One small step …

  • Splitting off data-access logic is necessary
    • MVC would also be nice, but let's leave it for now
  • The guys at KnpLabs have created a nice solution:
    • A base Knp\Repository class which you extend per entity
      • Define the name of the linked table on it
      • Use one of the built-in insert(), update(), delete(), find(), or findAll() functions
    • A RepositoryServiceProvider for use with Silex
    • Installation via Composer
      "require": {
      	"knplabs/repository-service-provider": "dev-master"
      }

RepositoryServiceProvider (1)

  • Base Knp\Repository class
    <?php
    
    namespace Knp;
    
    use Doctrine\DBAL\Connection;
    
    abstract class Repository {
    
        abstract public function getTableName();
    
        public $db;
    
        public function __construct(Connection $db) {
            $this->db = $db;
        }
    
        public function insert(array $data) {
            return $this->db->insert($this->getTableName(), $data);
        }
    
        public function update(array $data, array $identifier) {
            return $this->db->update($this->getTableName(), $data, $identifier);
        }
    
        public function delete(array $identifier) {
            return $this->db->delete($this->getTableName(), $identifier);
        }
        public function find($id) {
            return $this->db->fetchAssoc(sprintf('SELECT * FROM %s WHERE id = ? LIMIT 1', $this->getTableName()), array((int) $id));
        }
    
        public function findAll() {
            return $this->db->fetchAll(sprintf('SELECT * FROM %s', $this->getTableName()));
        }
    }
    

RepositoryServiceProvider (2)

  • Example class implementation
    namespace Ikdoeict\Repository;
    
    class TweetsRepository extends \Knp\Repository {
    	public function getTableName() { return 'tweets'; }
    }
  • Registration of the repositories within Silex
    $app->register(new Knp\Provider\RepositoryServiceProvider(), array(
    	'repository.repositories' => array(
    		'db.tweets' => 'Ikdoeict\\Repository\\TweetsRepository'
    	)
    ));
  • Usage
    $app['db.tweets']->findAll();

Example

Let's take a look at assets/ws2-sws-fiddles-silex/07.doctrine-organized/

(Be sure to install the example properly. @see the included README.md)

Example Recap

  • Create a Repository class per entity of your app
    • Place your classses in /src/Namespace/Repository
    • Name of class can be chosen freely, though something like TweetsRepository recommended
    • Classes must extend \Knp\Provider
    • The getTableName function must return the DB table name
  • Register your repositories in your app while registering RepositoryServiceProvider
    • Use db. as a prefix
Important: don't ever write queries in your ControllerProviders, but put 'm in your Repository classes!

More Service Providers

Not sure if ...

More built-in Service Providers

We'll take a closer look at these later on :)

Other Service-Providers

Questions?

Sources

ikdoeict.be