Serverside Webscripting [JLW384]

05.silex.part3

Built-in Service Provider:
UrlGeneratorServiceProvider

001100010010011110100001101101110011

UrlGeneratorServiceProvider

  • Silex Routes can be named
    $app->get('/', function () {
    	return 'welcome to the homepage';
    })->bind('homepage');
    
    $app->get('/hello/{name}', function ($name) {
    	return "Hello $name!";
    })->bind('hello');
  • The UrlGenerator Service Provider allows you to reference those routes in your App and via Twig

Inclusion

  • If you want to use the shorthand functions within Twig templates, require symfony/twig-bridge in composer.json
    "require": {
    	"symfony/twig-bridge": "2.*"
    }
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\UrlGeneratorServiceProvider());

Usage

  • In PHP
    $app['url_generator']->generate('homepage');
    $app['url_generator']->generate('hello', array('name' => 'Bramus'));
    // -> `/hello/Bramus`
  • In Twig
    {{ app.url_generator.generate('homepage') }}
  • In Twig, using twig-bridge
    {{ path('homepage') }} {# -> `/` #}
    {{ url('homepage') }} {# -> `http://example.org/` #}
    {{ path('hello', {name: 'Bramus'}) }} {# -> `/hello/Bramus` #}
    {{ url('hello', {name: 'Bramus'}) }} {# -> `http://example.org/hello/Bramus` #}

Sidenote: Static pages

  • Writing controllers for static pages is overkill. Here's a simple way to do it (in app.php)
$pages = array(
		'/' => 'home',
		'/about' => 'about'
	);
	foreach ($pages as $route => $view) {
		$app->get($route, function () use ($app, $view) {
			return $app['twig']->render('static/' . $view . '.twig');
		})->bind($view);
	}
}
Place static page templates in /src/views/static/

Example

Let's take a look at assets/ws2-sws-fiddles-silex/08.static-pages/

Example Recap

  • Place static page templates in /src/views/static/
  • Give routes a name using bind() within the connect() function of the controller
  • No need for {{ app.request.baseUrl }} in Twig anymore as {{ path('links') }} includes it
  • Extract parts of the URL in Twig using app.request.getPathInfo()|split('/')[1]

Built-in Service Provider:
MonologServiceProvider

Captain's log: We have lost control of the ship.
Adddendum: Whoooaaaa!

MonologServiceProvider

  • The MonologServiceProvider provides a default logging mechanism through the Monolog library.
  • Monolog log levels
    • DEBUG (100)
      • Detailed debug information
    • INFO (200)
      • Interesting events.
        e.g. User logs in, SQL logs
    • NOTICE (250)
      • Normal but significant events
    • WARNING (300)
      • Exceptional occurrences that are not errors.
        e.g. Use of deprecated APIs
    • ERROR (400)
      • Runtime errs that don't need immediate action
    • CRITICAL (500)
      • Critical conditions.
        e.g. unexpected exception
    • ALERT (550)
      • Action must be taken immediately.
        e.g.database unavailable
    • EMERGENCY (600)
      • Emergency: system is unusable

Inclusion

  • Require monolog/monolog in composer.json
    "require": {
    	"monolog/monolog": ">=1.0.0"
    }
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\MonologServiceProvider(), array(
    	'monolog.logfile' => __DIR__ . '/development.log',
    ));
    • Registration parameters
      • monolog.logfile — Location of logfile
      • monolog.level (optional) — Minimum log level before event is written in log. Default: Monolog\Logger::DEBUG
      • monolog.name (optional) — Name of the monolog channel.
        Default: myapp

Usage

  • Log events through $app['monolog']
    $app['monolog']->addDebug('Testing the Monolog logging.');
    $app['monolog']->addInfo(sprintf('User "%s" registered.', $username));
    $app['monolog']->addWarning('The use of global is deprecated');
    $app['monolog']->addError('Could not update item in database');
  • Sample output
    [2013-03-25 15:12:55] myapp.DEBUG: Testing the Monolog logging.
    [2013-03-25 15:12:56] myapp.INFO: User "Bramus" registered.
    [2013-03-25 15:12:57] myapp.WARNING: The use of global is deprecated
    [2013-03-25 15:12:58] myapp.ERROR: Could not update item in database

Middlewares

OK. We're in the middle of nowhere,
which is the safest part of nowhere.

Middlewares

  • Silex apps are executed in different stages
    • Bootstrapping of the app
    • Execution of the controller
    • Request handling
    • Output return
    • Process termination
  • Middleware = code executed in between the stages
  • Middlewares can be global, or only for a specific route
    • Application middleware
    • Route specific middleware

Before Application Middleware

A before application middleware allows you to tweak the Request before the controller is executed
  • Example
    $app->before(function (Request $request) use ($app) {
    	$app['twig']->addGlobal('first_url_part', explode('/', $request->getPathInfo())[1]);
    });
  • By default, the middleware is run after the routing and the security. To let it execute earlier use
    $app->before(function (Request $request) use ($app) {
    	// ...
    }, Application::EARLY_EVENT);

After Application Middleware

An after application middleware allows you to tweak the Response before it is sent to the client
  • Example
    $app->after(function (Request $request) use ($app) {
    	// ...
    });

Finish Application Middleware

A finish application middleware allows you to execute tasks after the Response has been sent to the client (like sending emails or logging)
  • Example
    $app->finish(function (Request $request, Response $response) {
    	// Warning: modifications to the Request or Response will be ignored
    });

Example

Let's take a look at assets/ws2-sws-fiddles-silex/09.middlewares/

Before Route Middleware

A before route middleware is fired just before the route callback, but after the before application middlewares
  • Example
    $app->get('/somewhere', function () {
    	// ...
    })->before(function (Request $request) use ($app) {
    	// ...
    });
  • Example (using a controller)
    public function connect(Application $app) {
    	return $app['controllers_factory']->get('/somewhere',
    	       array($this,'somewhere'))->before(array($this,'check'));
    }
    
    public function check(Request $request, Application $app) { /* ... */ }

After Route Middleware

An after route middleware is fired just after the route callback, but before the application its after application middlewares
  • Example
    $app->get('/somewhere', function () {
    	// ...
    })->after(function (Request $request, Response $response) use ($app) {
    	// ...
    });

Example

See further ;-)

Built-in Service Provider:
ValidationServiceProvider

I am the grand funeral director.
— Do you validate parking?

ValidationServiceProvider

  • The ValidatorServiceProvider provides a service for validating data.
  • It is most useful when used with the FormServiceProvider, but can also be used standalone.

Inclusion

  • Require symfony/validator in composer.json
    "require": {
    	"symfony/validator": "~2.3"
    }
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\ValidatorServiceProvider());

Usage

  • Uses Symfony's Validator which works with constraints
    • Blank
    • Choice
    • Collection
    • Date
    • DateTime
    • Email
    • False
    • File
    • Image
    • Ip
    • Max
    • Min
    • MaxLength
    • MinLength
    • NotBlank
    • NotNull
    • Null
    • Time
    • True
    • Url

  • Each constraint is a class in the namespace \Symfony\Component\Validator\Constraints

Warning!

Don't use the following examples to base your code upon. Only use them to get the gist of the constraints. When working with forms we'll use an other structure!

Validating Values

  • Validate a single value using
    $app['validator']->validateValue($val, constraints);
    use Symfony\Component\Validator\Constraints as Assert;
    
    $app->get('/validate/{email}', function ($email) use ($app) {
    	$errors = $app['validator']->validateValue($email, new Assert\Email());
    
    	if (count($errors) > 0) {
    		return (string) $errors;
    	} else {
    		return 'The email is valid';
    	}
    
    });

Validating Associative Arrays

  • Validate an associate array by building a constraints collection parallel to the array
    use Symfony\Component\Validator\Constraints as Assert;
    
    $book = array(
    	'title' => 'My Book',
    	'author' => array(
    		'first_name' => 'Fabien',
    		'last_name'  => 'Potencier',
    	),
    );
    
    $constraints = new Assert\Collection(array(
    	'title' => new Assert\Length(array('min' => 10)),
    	'author' => new Assert\Collection(array(
    		'first_name' => array(
    			new Assert\NotBlank(),
    			new Assert\Length(array('min' => 10))
    		),
    		'last_name'  => new Assert\Length(array('min' => 10)),
    	)),
    ));
    $errors = $app['validator']->validateValue($book, $constraints);
    
    if (count($errors) > 0) {
    	foreach ($errors as $error) {
    		echo $error->getPropertyPath().' '.$error->getMessage()."\n";
    	}
    } else {
    	echo 'The book is valid';
    }

Validating Objects

  • Check the documentation to validate objects

Built-in Service Provider:
FormServiceProvider

Guards! ... bring me the forms I need
to fill out to have her taken away!

FormServiceProvider

Inclusion

  • Require symfony/form and symfony/twig-bridge in composer.json
    "require": {
    	"symfony/form": "~2.3"
    	"symfony/twig-bridge": "~2.3"
    }
  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\FormServiceProvider());

Usage (1/2)

  • Use $app['form.factory'] to get an instance of Symfony's FormFactory
    $app->match('/form', function (Request $request) use ($app) {
    	// some default data for when the form is displayed the first time
    	$data = array(
    		'name' => 'Your name',
    		'email' => 'Your email',
    		'message' => 'Your message'
    	);
    
    	// Get the form
    	$form = $app['form.factory']->createBuilder('form', $data)
    		->add('name', 'text')
    		->add('email', 'email')
    		->add('message', 'textarea')
    		->getForm();
    
    	// Inspects the given request to see if the form was submitted or not
    	// If so it will transform and validate the data
    	$form->handleRequest($request);
    
    	// The form was submitted and is valid!
    	if ($form->isValid()) {
    
    		// Get the data
    		$data = $form->getData();
    
    		// Do something with the data
    		// …
    
    		// redirect somewhere
    		return $app->redirect('...');
    	}
    
    	// display the form
    	return $app['twig']->render('index.twig', array('form' => $form->createView()));
    });

Usage (2/2)

  • Twig template code
    <form action="#" method="post" {{ form_enctype(form) }}>
    	{{ form_widget(form) }}
    	<input type="submit" name="submit" />
    </form>
  • Resulting HTML
    <form action="#" method="post">
    	<div id="form">
    		<div>
    			<label for="form_name">Name</label>
    			<input type="text" id="form_name" name="form[name]" value="Your name">
    		</div>
    		<div>
    			<label for="form_email">Email</label>
    			<input type="text" id="form_email" name="form[email]" value="Your email">
    		</div>
    		<div>
    			<label for="form_message">Message</label>
    			<textarea id="form_message" name="form[message]">Your message</textarea>
    		</div>
    		<input type="hidden" id="form_token" name="form[_token]" value="bdb2fdb025d2a20fa21e4a3cbc6c8fda8051e020">
    	</div>
    	<input type="submit" name="submit" />
    </form>

Form Field Types

  • Form field types
    • checkbox
    • choice
    • csrf
    • date
    • datetime
    • hidden
    • text
    • textarea
    • email
    • integer
    • number
    • password
    • search
    • url
    • ...

Security Alert

  • CSRF = Cross-Site Request Forgery
  • CSRF = A serious type of attack (OWASP Top 10!)
A CSRF attack forces a logged-on victim’s browser to send a forged HTTP request, including the victim’s session cookie and any other automatically included authentication information, to a vulnerable web application. This allows the attacker to force the victim’s browser to generate requests the vulnerable application thinks are legitimate requests from the victim.
  • FIX: Generate a CSRF token on each form load (and limit that token to that form only) and validate it too.

Customizing forms (1/2)

  • To create a named form use an alternative function
    $loginform = $app['form.factory']->createNamed('loginform')
    	->add('username', 'text')
    	->add('password', 'password');
  • To pass in default data with named forms, pass it in as the 3rd parameter
    $data = array('username' => 'root', 'password' => 'Azerty123');
    
    $loginform = $app['form.factory']->createNamed('loginform', 'form', $data)
    	->add('username', 'text')
    	->add('password', 'password');

Customizing forms (2/2)

  • It's possible to customize the output of a form
    <form action="#" method="post" {{ form_enctype(loginform) }}>
    	<dl class="clearfix">
    		<dt>
    			{{ form_label(loginform.username) }}
    		</dt>
    		<dd>
    			{{ form_widget(loginform.username) }}
    			{{ form_errors(loginform.username) }}
    		</dd>
    		<dt>
    			{{ form_label(loginform.password) }}
    		</dt>
    		<dd>
    			{{ form_widget(loginform.password) }}
    			{{ form_errors(loginform.password) }}
    		</dd>
    	</dl>
    	{{ form_widget(loginform._token) }}
    	<input type="submit" name="submit" value="Log In" />
    </form>
    Note: Don't forget the CSRF token!!

Forms + Validator = Winning!

  • FormServiceProvider plays nice with ValidatorServiceProvider
    use Symfony\Component\Validator\Constraints as Assert;
    
    …
    
    $loginform = $app['form.factory']->createNamed('loginform', 'form', $data)
    	->add('username', 'text', array(
    		'constraints' => new Assert\Email()
    	))
    	->add('password', 'password', array(
    		'constraints' => array(
    			new Assert\NotBlank(),
    			new Assert\Length(array('max' => 30))
    		)
    	));

Forms + Validator Prerequisites

  • Require a few more packages in composer.json though
    "require": {
    	"symfony/form": "~2.3",
    	"symfony/validator": "~2.3",
    	"symfony/twig-bridge": "~2.3",
    	"symfony/config": "~2.3",
    	"symfony/translation": "~2.3"
    }
  • Register the TranslationServiceProvider in bootstrap.php (with emtpy messages) or it won't work
    $app->register(new Silex\Provider\TranslationServiceProvider(), array(
    	'translator.messages' => array(),
    ));

Example

Let's take a look at assets/ws2-sws-fiddles-silex/10.forms-validation/

Example sidenotes (1/3)

  • Add the novalidate attribute to the form to prevent HTML5 formchecking.
  • Labels default to the name of the input. Change it via label or from within Twig
    $addlinkform->add('url', 'url', array('label' => 'The URL'));
    {{ form_row(addlinkform.url, { 'label': 'The URL' }) }}

Example sidenotes (2/3)

  • When using date as a type
    • Symonfy will output three dropdowns which is not recommended (UX!)
      • Set widget to single_text to get one single textfield
    • You can manipulate the format via format
    $registerform->add('date', 'date', array(
    	'widget' => 'single_text',
    	'format' => 'YYYY-mm-dd'
    ));
  • It's also possible to check one single field for validity
    if (!$registerform->get('email')->isValid()) { … }

Example sidenotes (3/3)

  • It's also possible to add/display errors for the entire form (instead of per field)
    $addlinkform->addError(new \Symfony\Component\Form\FormError('That link was already entered'));
    {{ form_errors(addlinkform) }}
    {{ form_widget(addlinkform.title) }}
    {{ form_widget(addlinkform.url) }}
    {{ form_widget(addlinkform._token) }}

File Uploads

$app->match('/', function (Application $app) {

	$uploadform = $app['form.factory']
		->createNamed('uploadform')
		->add('my_upload', 'file', array(
			'constraints' => new Assert\NotBlank()
		));

	$uploadform->handleRequest($app['request']);

	if ($uploadform->isValid()) {
		$files = $app['request']->files->get($uploadform->getName());
		$filename = $files['my_upload']->getClientOriginalName();
		$files['my_upload']->move(__DIR__ . '/../public_html/uploads/', $filename);

		return $app['twig']->render('form.twig', array(
			'message' => 'File was successfully uploaded!',
			'uploadform' => $uploadform->createView()
		));
	}

	return $app['twig']->render('form.twig', array(
		'message' => 'Upload a file',
		'uploadform' => $uploadform->createView()
	));

})->method('GET|POST');

Example

Let's take a look at assets/ws2-sws-fiddles-silex/11.session-upload/

Built-in Service Provider:
SessionServiceProvider

Now Stop! Hammertime!

SessionServiceProvider

  • The SessionServiceProvider provides a service for storing data persistently between requests

Inclusion & Usage

  • Register the service in bootstrap.php
    $app->register(new Silex\Provider\SessionServiceProvider());
  • Use $app['session'] to get and set session data
    $app['session']->set('name', 'Bramus');
    echo $app['session']->get('name'); // Bramus

Example

Again take a look at assets/ws2-sws-fiddles-silex/11.session-upload/

ConfigServiceProvider

If we can re-route engine power through the primary weapons and configure them to Melllvar's frequency, that should overload his electro-quantum structure.

ConfigServiceProvider

  • Instead of defining configuration parameters using define() use the custom ConfigServiceProvider
  • Works with configuration files in PHP, JSON or YAML
  • Injects the parameters onto $app

Inclusion

  • Require igorw/config-service-provider in composer.json
    "require": {
    	"igorw/config-service-provider": "~1.1"
    }
  • Register the service in bootstrap.php referencing the config
    $app->register(new Igorw\Silex\ConfigServiceProvider(__DIR__ . '/config.php'));
Don't ever place your config file in the web folder!

Example Config Files (1/2)

  • PHP
    return array(
    	'db.options' => array(
    		'dbname' => 'todolist',
    		'user' => 'root',
    		'password' => 'Azerty123',
    		'host' => 'localhost',
    		'driver' => 'pdo_mysql',
    		'charset' => 'utf8mb4'
    	),
    	'myproject.test' => array(
    		'param2' => '456',
    		'param3' => array(
    			'param2B' => '456',
    			'param2C' => '456',
    		),
    		'param4' => array(4, 5, 6)
    	)
    );
  • JSON
    {
    	"db.options": {
    		"dbname": "todolist",
    		"user": "root",
    		"password": "Azerty123",
    		"host": "localhost",
    		"driver": "pdo_mysql",
    		"charset": "utf8mb4"
    	},
    	"myproject.test": {
    		"param2": "456",
    		"param3": {
    			"param2B": "456",
    			"param2C": "456"
    		},
    		"param4": [4, 5, 6]
    	}
    }

Example Config Files (2/2)

  • YAML
    ---
      db.options:
        dbname: "todolist"
        user: "root"
        password: "Azerty123"
        host: "localhost"
        driver: "pdo_mysql"
        charset: "utf8mb4"
      myproject.test:
        param2: "456"
        param3:
          param2B: "456"
          param2C: "456"
        param4:
          - 4
          - 5
          - 6
  • Usage: $app['db.options']['user']

Questions?

Sources

ikdoeict.be