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