Symfony2 Logo

Welcome the BCCCronManagerBundle, the Symfony2 bundle that helps you managing your crons

One thing I don’t like to do when maintaining a website, is having to pull out ssh on a daily basis in order to check that everything is running fine. A basic use case is scheduling and watching crons.

So I recently came out with the idea of building a web interface wrapping the use of the crontab command with some tools for watching associated log files.

And thus, the BCCCronManagerBundle was born.

A quick presentation

The BCCCronManagerBundle can already do some nice things:

  • display the cron entries of the cron table, parsing time expression, command, output file, error file and comment
  • guess the last execution time and status
  • display log files
  • add, edit and remove cron entries

The bundle is localized in english and french. The forms also include some shortcuts t easily build common time expression, launch a symfony command or log in the symfony log directory.

The cron list

The cron list

The cron form

The cron form

A cron log file

A cron log file


How it works

Actually, the architecture is quite simple. Everything relies on two classes: CronManager and Cron.

The CronManager launches the [cci]crontab -l[/cci] command in constructor, then extracting each lines in order to build a collection of Cron instance. It has get, add and remove methods in order to access the Cron collection. A raw method build up the cron table string based on the Cron collection and a write method puts it into a temporary file before launching the [cci]crontab $file[/cci] command.

The Cron class is instantiated using the parse static method. Its job is to parse a cron line from the cron table and extract the time expression, command, output, error output and comment (if defined). Based on the output files it can guess if the cron has already been runned (one of the output files is present) and if it was successful or not (the error file is empty). A getExpression method can build the time expression and the _toString is overriden in order to give the cron representation for the cron table.

The interface is quite neat, thanks to Sam. He helped me implementing the twitter bootstrap which is very powerfull and elegant. I also decided to make use of the jQuery plugin that is quite impressive and can easily replace jQueryUI on some points.

Wrap up

You can download and install the bundle on the github : https://github.com/michelsalib/BCCCronManagerBundle.
I also welcome contributions for any improvement, such as a better cron table parsing, more options for cron definition (such as log files), better support of multi platforms, or translations.

Symfony2 Logo

A twig extension that translates countries and dates

For today’s tutorial, we will see how to create a Twig extension that provides two filters that localize countries and dates.

What is wrong
In Symfony2, you might run into this issue. For instance, countries that are saved by form using the CountryType are stored using a two letter code. For instance you can have US for “United States”, UK for “United Kingdom”… The problem is that you cannot use it directly; you need a read string representation. Moreover, this string representation varies depending on the current locale.
Also you will have exactly the same trouble with dates, which can be “January 1rst” in English and “1er Janvier” in French.

What we need
The easiest way to handle this kind of translation is to use a twig filter. At the end we want this syntax:
– [cci]{{ user.coutnry | country }}[/cci]
– [cci]{{ user.birthday | localeDate }}[/cci]

Note that such a translation cannot be done easily without the use of the apache2 intl module. You have to install it on your server before going any further : http://www.php.net/manual/en/book.intl.php.

Create a twig extension
Adding filters into Symfony2 is quite simple. You need to create a new class that override the [cci]Twig_Extension[/cci] class.

Then we can override the [cci]getFilters[/cci] and the [cci]getName[/cci] functions.
[cc_php]
new Twig_Filter_Function(
‘MyVendorMyBundleTwigTwigExtension::countryFilter’
),
‘localeDate’ => new Twig_Filter_Function(
‘MyVendorMyBundleTwigTwigExtension::localeDateFilter’
),
);
}

public function getName()
{
return ‘myExtensionName';
}
}
[/cc_php]

Note that at this moment the country and the localeDate are defined. When we will use them from a template they will call the defined static functions.

The country filter
The country function will use the list of countries already used by the Symfony2 form component. It is the [cci]SymfonyComponentLocaleLocale[/cci] class:
[cc_php]
<?php
namespace MyVendorMyBundle Twig;

use SymfonyComponentLocaleLocale;

class TwigExtension extends Twig_Extension {

//…

public static function countryFilter($country)
{
$countries = Locale::getDisplayCountries(
Locale::getDefault()
);

return $countries[$country];
}
}
[/cc_php]

The code is pretty straightforward. Just note that to obtain the application locale we use the [cci] Locale::getDefault()[/cci] function, this [cci]Locale[/cci] class is provided by the intl extension. Also when Twig calls the filter, it always sets as first parameter the filtered value.

The localeDate filter
The filter for the date is a little more complicate:
[cc_php]
IntlDateFormatter::NONE,
‘short’ => IntlDateFormatter::SHORT,
‘medium’ => IntlDateFormatter::MEDIUM,
‘long’ => IntlDateFormatter::LONG,
‘full’ => IntlDateFormatter::FULL,
);
$dateFormater = IntlDateFormatter::create(
Locale::getDefault(),
$values[$dateType],
$values[$timeType]
);

return $dateFormater->format($date);
}
}
[/cc_php]

First of all we added two more optional parameters. They will serve to override the kind of rendering we want for the filtered date. We can use them like this :
– [cci]{{ user.createdAt | localeDate(‘long’,’medium’) }}[/cci] for a long date and a medium time representation

Then we create a date formatter which is an instance of [cci]IntlDateFormatter[/cci] parameterized with our format parameters. I used an associative array to translate string parameters to the supported contants.

Then we can simply format the date and return it using the [cci]format[/cci] function.

Activate the twig extension
In order to activate the twig extension, you need to register it as a service to the container. In order for the container to know it is a twig extension, you need to add the [cci]twig.extension [/cci] tag.

Just add to your [cci]config.yml[/cci] file:
[cc]
services:
bcc.twig.extension:
class: MyVendorMyBundleTwigTwigExtension
tags:
– { name: twig.extension }
[/cc]

Wrap up
We build a nice powerful twig extension that provides two filters that helps localize countries and dates.

You simply add the extension to your project by checking out the BCCExtraToolsBundle : https://github.com/michelsalib/BCCExtraToolsBundle.

Symfony2 Logo

Symfony2: A translation message extractor command

One of the very painful taks you may face using symfony2 is the extration of all you translation message from your twig templates. This is much more annoying knowing that symfony1.4 did the job for you with a simple command, which does not exist in symfony2.

Today I will give give you a command for symfony2 that checks all your twig messages, combine them with your already existing messages in your yaml translations files and save the new ones. It is a recent work for me and it just works with twig/yml files.

The Command
I embedded it in a Bundle on github: https://github.com/michelsalib/ExtraToolsBundle.

You just need to get it, register the namespace and the bundle.

The name of the command is [cci]bcc:trans:update locale bundleName[/cci], where the [cci]locale[/cci] is the targeted locale (en, fr, es…) and the [cci]bundleName[/cci] is the name of the targeted bundle. You have several options:
– –dump-messages to display your final messages
– –force to update/write your translation files, it also perform a backup of the old ones
– –prefix=”…” if you want to change the prefix use for your new messages, by default [cci]__ [/cci] is used

Some example:
– To extract the messages of your bundle and display them in the console:
[cci]bcc:trans:update –dump-messages fr MyBundle[/cci]

- You can save them:
[cci]bcc:trans:update –force fr MyBundle[/cci]

- In another language:
[cci]bcc:trans:update –force en MyBundle[/cci]

- Or if you want to chaneg the prefix used to generate the new messages:
[cci]bcc:trans:update –force –prefix=’myprefix’ en MyBundle[/cci]

Behind the scene
The trick behind the code is how to properly crawl your twig templates.

First to get them:
[cc lang="php"]
// get bundle directory
$foundBundle = $this->getApplication()->getKernel()->getBundle(‘MyBundle’);
// get twig templates
$finder = new Finder();
$files = $finder->files()->name(‘*.html.twig’)->in($foundBundle->getPath() . ‘/Resources/views/’);
// iterate over the files
foreach ($files as $file) {
$path = $file->getPathname();
// parse the files
}
[/cc]

Here you just need to use the kernel to retrieve the bundle data from a simple bundle name. When you have the bundle path, you can use the finder to get all the file ending by [cci].html.twig[/cci] in the relative [cci]/Resources/views/[/cci] directory.

You can now parse the twig files:
[cc lang="php"]
$tree = $twig->parse($twig->tokenize(file_get_contents($path)));
[/cc]

Then you get a tree composed of [cci]Twig_Node[/cci]. The rest is just an algorithmic problem using recursion and type checking to find [cci]SymfonyBridgeTwigNodeTransNode[/cci] (for [cci]{% trans %}…{% end trans %} syntax[/cci]) and [cci]Twig_Node_Print[/cci] that contains trans filter (for [cci]{{ … | trans }}[/cci] syntax).

When you finally have all your messages, you might want to save them into yaml. For that you have two very simple static functions:
[cc lang="php"]
// get a yaml file into a php array
$array = SymfonyComponentYamlYaml::load($path);
// transform an array into a yaml string
$yml = SymfonyComponentYamlYaml::dump($array);
[/cc]

Wrap up
I hope this command will help you building better symfony2 app, by automatizing some work. Don’t hesitate to report me bugs, suggestions or to fork me on github!

Symfony2 Logo

Advance customization of the 403 error page in Symfony2

If you currently are testing the brand new Symfony2 framework, you might have tried to customize the 403 page. Actually, you have two documented ways to do so, first by overriding the default exception template, second by listening to the onCoreException event and filtering the right exception.

Unfortunately, neither of these two methods satisfied me. The simple customization of the template was not enough for my needs and the addition of an ExceptionListener impose a “maybe” to much complex filtering in order to retrieve just the 403 exception.

Note: Actually, my needs are similar to those I exposed in this blog post on symfony 1.4 : Symfony 1.4 vs. sfGuardUserPlugin : understand credentials and permissions.

What I want to do is to get the unsatisfied requirements and display them to the user.

First secure an action
In order to secure the access to my action with specific requirements, I use the @Secure annotation on my controller’s method. The annotation is provided by the JMSSecurityExtraBundle included in the symfony2 standard edition:

[php]
// /src/MyVendor/MyBundle/Controller/MyController.php

/**
* …
* @extra:Secure(roles="ROLE_MEMBER")
*/
public function newAction()
{
// …
}
[/php]

Here in order to access my action, the user will need to have the role named ROLE_MEMBER. If not, the security layer will throw an Exception resulting in a 403 response.

Change the unauthorized action
You can change the action that will display the 403 page in the security configuration:

[yml]
# /app/config/security.yml
security:
access_denied_url: /unauthorized
[/yml]

Now when dealing with an unauthorized Exception, the security layer will call the action associated to this URL. You need to specify the action that matches this URL in your routing configuration:

[yml]
# /src/MyVendor/MyBundle/Resources/config/routing.yml
unauthorized:
pattern: /unauthorized
defaults: { _controller: FrontendBundle:Default:unauthorized }
[/yml]

Now, the action responsible for rendering a 403 page is the following:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

class DefaultController extends Controller
{
// …

/**
* @extra:Template()
*/
public function unauthorizedAction()
{
return array();
}
}
[/php]

And the associated template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

403 error: Unauthorized

[/html]

Display the unmatched requirements
Now, the last things I want to do are to retrieve the required roles of the original request and display them to the user, so that he knows why his request did not succeed.
The tricky now is to obtain the original request, deduce the controller, load its annotations and thus deduce his requirements. Here is what I did:

[php]
// /src/MyVendor/MyBundle/Controller/DefaultController.php

public function unauthorizedAction()
{
// get the previous requests
$requests = $this->container->getCurrentScopedStack(‘request’);

// get the previous controller
$controllerResolver = new SymfonyComponentHttpKernelControllerControllerResolver();
list($controller, $method) = $controllerResolver->getController($requests['request']['request']);
// isolate the method
$method = new ReflectionMethod($controller, $method);
// load the metas
$reader = new JMSSecurityExtraBundleMappingDriverAnnotationReader();
$converter = new JMSSecurityExtraBundleMappingDriverAnnotationConverter();
$annotations = $reader->getMethodAnnotations($method);
$metadata = $converter->convertMethodAnnotations($method, $annotations)->getAsArray();
// isolate the required roles
$requiredRoles = $metadata['roles'];

return array(‘requiredRoles’ => $requiredRoles);
}
[/php]

The first thing I do is to obtain from the container the request stack that contains the original request. From the request I use the ControllerResolver in order to retrieve the initial controller and then isolate the method. With this method, I can use the AnnotationReader and AnnotationConverter from the JMSSecurityExtraBundle and get back the required roles.
Despite the fact that the method is pretty straightforward, it is not as elegant as I wanted it to be (in the whole process, the controller resolving and annotation extraction will be done twice). But it does the job.

The final thing to do is the show the required roles to the user in the template:

[html]
{# /src/MyVendor/MyBundle/Resources/views/Default/unauthorized.html.twig #}

Required roles:

{% for role in requiredRoles %}
{{ role }}

{% endfor %}

[/html]