Serverside Webscripting [JLW384]

03.silex.part1

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.

Silex Introduction

Silex

  • PHP microframework
  • Built around routes
    • When requesting / show the homepage
    • When requesting /news show all newsitems
    • When requesting /news/{id} show the proper newsitem
    • When requesting /news/{id} via POST add a comment
  • Extensible using Providers
    • Twig Service Provider
    • Doctrine Service Provider
    • Swiftmailer Service Provider

Installation

  • Installable via Composer
    $ composer require silex/silex:~1.1

Code Example

  • The core of Silex is an instance of Silex\Application
    • Register providers on it
    • Define routes on it
require_once __DIR__ . '/../vendor/autoload.php';

$app = new Silex\Application();

$app->get('/hello/{name}', function($name) use($app) {
	return 'Hello ' . $app->escape($name);
});

$app->run();

Silex Internals

  • Internally, The Silex Core is built on top of

Silex Boilerplate & Bootstrapping

If robots can't go to heaven, heaven can come to us.

Silex Boilerplate & Bootstrapping

  • Basic Silex Project Structure
    • public_html/ — Public files
      • index.php — Front Controller
      • css/ & js/ & img/ & files/ &
      • .htaccess (or the like)
    • app/ — Project logic
      • bootstrap.php — App Bootstrap
      • app.php — Routing
    • vendor/(as created by Composer)
    • composer.json & composer.lock
Note: Make sure only the contents of public_html/ are accessible over HTTP!

Example

Let's take a look at the files in assets/ws2-sws-fiddles-silex/00.basic/

Example Recap

  • /public_html/index.php is the Front Controller
    <?php
    require_once __DIR__ . '/../app/app.php';
    $app->run();
  • /app/app.php contains the actual routing
    <?php
    require __DIR__ . '/bootstrap.php';
    $app->get('/', function(Silex\Application $app) {
    	return 'ohai';
    });
  • /app/bootstrap.php creates & configures the $app
    <?php
    require_once __DIR__ . '/../vendor/autoload.php';
    $app = new Silex\Application();
    $app['debug'] = true;

Silex Routing

Now here's a route with some chest hair.

Routing

  • Definitions
    • A route is a combination of a pattern and an HTTP method
    • A route is handled by a handling function which returns/does x/y
  • In Silex
    $app->match('pattern', function(Silex\Application $app) {
    	// ...
    })->method('GET|POST');
  • Shorthands (preferred)
    $app->get('pattern', function(Silex\Application $app) {
    	// ...
    });
    $app->post('pattern', function(Silex\Application $app) {
    	// ...
    });

Static Routing

  • Static routes are routes with a fixed pattern
    $app->get('/', function(Silex\Application $app) {
    	// show something (e.g. return a blob of HTML)
    });
    
    $app->get('/about/', function(Silex\Application $app) {
    	// show something else (e.g. return a blob of HTML)
    });
    
    $app->post('/', function(Silex\Application $app) {
    	// do something (e.g. insert into database, send e-mail)
    });
    
Note: Only the first route matched will be used

Dynamic Routing

  • Dynamic routes are routes with a dynamic pattern
    • The dynamic parts of the pattern will be converted to route variables which you pass into the handling function
    $app->get('/hello/{name}/', function(Silex\Application $app, $name) {
    	return 'Hello ' . $app->escape($name) . '!';
    });
    $app->get('/blog/{postId}/{commentId}/', function (Silex\Application $app, $postId, $commentId) {
    	// ...
    });
Important: use $app->escape() to prevent XSS!

Redirecting and Error Handling

  • Redirect using $app->redirect()
    $app->get('/', function(Silex\Application $app) {
    	return $app->redirect($app['request']->getBaseUrl() . '/hello');
    });
  • Override the default error handling using $app->error()
    $app->error(function (\Exception $e, $code) {
    	if ($code == 404) {
    		return 'Please visit <code>/hello/<em>name</em></code>';
    	} else {
    		return 'Shenanigans! Something went horribly wrong';
    	}
    });

Important note on route patterns

  • Define your patterns with a trailing /
    • $app->get('/hello', …); will respond to http://localhost/hello but not to http://localhost/hello/
    • $app->get('/hello/', …); will respond to http://localhost/hello and to http://localhost/hello/

Example

Let's take a look at the files in assets/ws2-sws-fiddles-silex/01.hello/

Route Prerequisites

  • It's possible to
    • Set requirements for variables in routes
    • Set defaults for variables in routes
    • Preprocess variables in routes using converters

Example

Let's take a look at the files in assets/ws2-sws-fiddles-silex/02.olleh/

Route Prerequisites, Redux

  • Not covered in the example, but also possible:
    • Limit routes on a host using ->host()
      $app->match('/', function() {
          // app-specific action
      })->host('example.com');
      
      $app->match('/', function ($user) {
          // user-specific action
      })->host('{user}.example.com');
    • Require HTTP/HTTPS using ->requireHttp() and ->requireHttps()
    • Convert variables to a certain type using ->convert()
      $app->get('/user/{id}', function ($id) {
          // ...
      })->convert('id', function ($id) { return new User($id); });

Sidenote: Request & Response (1)

  • As said, Silex uses Symfony Components internally
    • The aforementioned $app['request'] actually is an instance of Symfony\Component\HttpFoundation\Request
    • Next to Request, Silex also uses Symfony's Response
    use Symfony\Component\HttpFoundation\Request;
    use Symfony\Component\HttpFoundation\Response;
    
    $app->post('/feedback', function(Request $request) {
    	$message = $request->get('message');
    	mail('feedback@yoursite.com', '[YourSite] Feedback', $message);
    
    	return new Response('Thank you for your feedback!', 201);
    });
Note: When returning a string, Silex converts it to a Response with status code 200

Sidenote: Request & Response (2)

  • It's also possible to send other types of responses
    • JSON: JsonResponse or use the $app->json() shorthand
      $app->get('/users/{id}', function(Silex\Application $app, $id) {
      	return $app->json(getUser($id));
      });
    • Files: BinaryFileResponse or use the $app->sendFile() shorthand
      $app->get('/files/{path}', function(Silex\Application $app, $path) {
      	if (!file_exists('/base/path/' . $path)) { return 'Invalid File'; }
      	return $app->sendFile('/base/path/' . $path);
      });

Structured Routing

Number 1.0, I hereby petition you
for an emergency sort-and-file!

Base Example

Let's take a look at the files in
assets/ws2-sws-fiddles-silex/03.tweets/

Example Analysis (1)

  • Pass extra data to handling functions with use
    $app->get('/tweets', function(Silex\Application $app) use ($tweets) {
    	$output = '<ul>';
    	foreach ($tweets as $tweet) {
    		$output .= '<li><a href="' . $app['request']->getBaseUrl(). '/tweets/'
    				. $app->escape($tweet['id']) . '">&para;</a> '
    				. $app->escape($tweet['text']) . '</li>';
    	}
    	$output .= '</ul>';
    	return $output;
    });
  • Trigger errors with $app->abort()
    $app->get('/tweets/{id}', function(Silex\Application $app, $id) use ($tweets) {
    	if (!in_array($id, array_column($tweets, 'id'))) {
    		$app->abort(404, "Tweet $id does not exist");
    	}
    	...
    })->assert('id', '\d+');

Example Analysis (2)

  • Codebase doesn't scale well
    • What if we need to define more logic/routes?
      /app/app.php will quickly become one cluttered mess
    • Solution: Organize our routes in a ControllerCollection
  • Building strings of HTML in your PHP is plain stupid
    • Solution: introduce Twig or the like
Note: We won't be introducting Twig for now, we'll tackle it later on

Route Controllers (1)

  • Group routes in a ControllerCollection
  • Mount the ControllerCollection instance onto a base route
    // define controller for a blog
    $blog = $app['controllers_factory'];
    
    $blog->get('/', function () {
    	return 'Blog home page';
    });
    $blog->get('/{id}', function () {
    	return 'Blog detail page';
    })->assert('id', '\d+');
    
    // Mount the blog controller onto the /blog route
    $app->mount('/blog', $blog);
Note: $app['controllers_factory'] returns a new ControllerCollection instance

Route Controllers (2)

Route Controllers (3)

  • Silex' structured answer: ControllerProviders in which you wrap a ControllerCollection
    • If we namespace our own controllerproviders properly, we'll have a good structure

Revised Example

Let's take a look at the files in
assets/ws2-sws-fiddles-silex/04.tweets-organized/

Example Recap (1)

  1. Create a namespace of your own
    • Place your namespace in /src
    • Autoload your namespace by adjusting composer.json
      {
      	"require": {
      		"silex/silex": "~1.1"
      	},
      	"autoload": {
      		"psr-0": {
      			"Ikdoeict": "src/"
      		}
      	}
      }
    • Important: Run composer update if you've already run composer install

Example Recap (2)

  1. Create a ControllerProvider per group of routes
    • The class must implement Silex\ControllerProviderInterface
    • Name of the class can be chosen freely, though something like TweetsController recommended
    • Place the file in /src/Namespace/Provider/Controller
    • The connect function regulates the (sub)routes to other instance functions
  2. Mount your class on the subroute from /app/app.php
    $app->mount('/tweets', new Ikdoeict\Provider\Controller\TweetsController());

Questions?

Sources

ikdoeict.be