Introduction

The Symfony2 RAD Edition is based on the work of the KnpRadBundle.

Its purpose is to provide a more opiniated edition of Symfony2 than the standard one. Basically, it accelerates some processes during your development that we think are recurrents, here at KnpLabs.

We believe that these enhancements allow us to quickly prototype and deliver better tested projects.

Feel free to collaborate and give us your opinion about this edition.

Many thanks to all the awesome contributors of this bundle!

Loading...

What happenned to the previous KnpRadBundle?

Legacy version of the KnpRadBundle can be found on Github but please note that it is no longer maintained and this new version intends to replace it.

Have fun!

KnpLabs

Installation of RAD edition

We recommend to use Composer in order to create a new project.

Once installed, run the following commands to create a new project:

$ composer.phar create-project -s dev --prefer-dist --dev knplabs/rad-edition my_project
$ cd my_project

Installation of RAD bundle

If you want to use the bundle in the existing project, you can do the following:

$ composer.phar require knplabs/rad-bundle@~2.3.2
<?php
//app/AppKernel.php
class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = array(
            //...
            new Knp\RadBundle\KnpRadBundle(),
        );
    }

}
#app/config/routing_dev.yml
_knp_rad_assistant:
    resource: "@KnpRadBundle/Resources/config/routing/assistant.xml"
    prefix: /_assistant
#app/config/routing.yml
acme_demo:
    resource: "@AcmeDemoBundle/Resources/config/routing.yml"
    type:     rad_convention
    prefix:   /


About Routing you can read more here and in Wiki

Bundle and configuration

# app/config/config.yml
app:
    foo: 'bar' # default to bar

<?php

namespace App\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AppExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();

        $parsedConfig = $this->processConfiguration($configuration, $configs);

        $container->addParameter('app.foo', $parsedConfig['foo']);
    }
}

<?php

namespace App\DependencyInjection;

use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('app');

        $rootNode
            ->children()
                ->scalarNode('foo')
                    ->defaultValue('bar')
                ->end()
            ->end()
        ;

        return $treeBuilder;
    }
}
<?php

use Knp\RadBundle\AppBundle\Bundle;
use Symfony\Component\Config\Definition\Builder\NodeParentInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class App extends Bundle
{
    public function buildConfiguration(NodeParentInterface $rootNode)
    {
        $rootNode
            ->children()
                ->scalarNode('foo')
                    ->defaultValue('bar')
                ->end()
            ->end()
        ;

    }

    public function buildContainer(array $config, ContainerBuilder $container)
    {
        // here $config is the parsed configuration
        $container->setParameter('app.foo', $config['foo']);
    }
}

Template resolution

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class BlogPostsController extends Controller
{
    public function indexAction()
    {
        return $this->render('App:BlogPosts:index', [
            'blogPosts' => $this->getBlogPosts()
        ]);
    }
}
<?php

namespace App\Controller;

class BlogPostsController
{
    public function indexAction()
    {
        return ['blogPosts' => $this->getBlogPosts()];
    }
}

Form Manager

Wiki

<?php

public function newAction(Request $request)
{
    $post = new BlogPost();
    $form = $this->createForm(new NewBlogPostType(), $post);

    if (!$request->isMethodSafe() && $form->handleRequest($request) && $form->->isValid()) {
        $this->getDoctrine()->getManager()->persist($post);
        $this->getDoctrine()->getManager()->flush();
        $request->getSession()
            ->getFlashBag()
            ->add('success', 'app_blogposts_new.success')
        ;

        return $this->redirect(
            $this->generateUrl('app_blogposts_index')
        );
    }

    return $this->render('App:BlogPosts:new.html.twig', array(
        'form' => $form->createView(),
    );
}
<?php

public function newAction()
{
    $post = new BlogPost;
    $form = $this->createBoundObjectForm($post, 'new');

    if ($form->isBound() && $form->isValid()) {
        $this->persist($post, true);
        $this->addFlash('success');

        return $this->redirectToRoute('app_blogposts_index');
    }

    return ['form' => $form->createView()];
}

Routing

Wiki

homepage:
    pattern: /
    defaults: { _controller: FrameworkBundle:Template:template, template: 'App::homepage.html.twig' }

app_blogposts_index:
    pattern: '/blogposts'
    defaults: { _controller: 'App:BlogPosts:indexAction' }
    requirements: { _method: 'GET' }

app_blogposts_new:
    pattern: '/blogposts/new'
    defaults: { _controller: 'App:BlogPosts:newAction' }
    requirements: { _method: 'GET|POST' }

app_blogposts_show:
    pattern: '/blogposts/{id}'
    defaults: { _controller: 'App:BlogPosts:showAction' }
    requirements: { _method: 'GET' }

app_blogposts_edit:
    pattern: '/blogposts/{id}/edit'
    defaults: { _controller: 'App:BlogPosts:editAction' }
    requirements: { _method: 'GET|PUT' }

app_blogposts_delete:
    pattern: '/blogposts/{id}'
    defaults: { _controller: 'App:BlogPosts:deleteAction' }
    requirements: { _method: 'DELETE' }
homepage:
    pattern: /
    defaults: { _controller: FrameworkBundle:Template:template, template: 'App::homepage.html.twig' }

'App:BlogPosts':
    defaults:
        _resources:
            "blogPosts": {service: "app.entity.blog_post_repository", method: "findAll"}
            "blogPost":  {service: "app.entity.blog_post_repository", method: "find", arguments: [name: "id"]}
            "session":   {service: "service_container",               method: "get",  arguments: [value: "session"]}

Resource resolver

Wiki

<?php

namespace App\Controller;

class BlogPostsController
{
    public function indexAction()
    {
        $blogPosts = $this->getDoctrine()
            ->getManager()
            ->getRepository('App:Blogpost')
            ->findAll()
        ;

        return ['blogPosts' => $blogPosts];
    }
}
<?php

namespace App\Controller;

class BlogPostsController
{
    public function indexAction(array $blogPosts)
    {
        return ['blogPosts' => $blogPosts];
    }

    public function showAction(BlogPost $blogPost, Session $session)
    {
        return ['blogPost' => $blogPost];
    }
}

Dependency Injection

<?php

namespace MyBundle\DependencyInjection;

use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;

class MyBundleExtension extends Extension
{
    public function load(array $configs, ContainerBuilder $container)
    {
        $loader = new YamlFileLoader(
            $container,
            new FileLocator(__DIR__.'/../Resources/config')
        );
        $loader->load('services.yml');

        $environment = $container->getParameter('kernel.environment');
        if ('test' === $environnement) {
             $loader->load('services_test.yml');
        }
    }
}

# Resources/config/services.yml
parameters:
   my_bundle_blogpost_manager.class: 'Mybundle\Manager\BlogPostManager'

services:
    my_bundle_blogpost_manager:
        class: '%my_bundle_blogpost_manager.class%'

# Resources/config/services_test.yml
parameters:
   my_bundle_blogpost_manager.class: 'Mybundle\Manager\FakeBlogPostManager'
# Resources/config/services.yml
parameters:
   my_bundle_blogpost_manager.class: 'Mybundle\Manager\BlogPostManager'

services:
    my_bundle_blogpost_manager:
        class: '%my_bundle_blogpost_manager.class%'

# Resources/config/services_test.yml
parameters:
   my_bundle_blogpost_manager.class: 'Mybundle\Manager\FakeBlogPostManager'

Auto registration of services

All auto-detected services that implement ContainerAwareInterface will receive the service container automatically.
Any auto-generated services can be overrided by simply defining a service with the same id.
Classes that doesn't extend/implement correct class/interface won't be auto-registered.

Security voters

src
└── App
    └── Security
        └── BlogPostVoter.php # implements Symfony\Component\Security\Core\Authorization\Voter\VoterInterface
                              # and Symfony\Component\DependencyInjection\ContainerAwareInterface

services:
    security.access.blacklist_voter:
        class: 'App\\Security\\BlogPostVoter'
        public: false
        arguments: [@service_container]
        tags:
            - { name: security.voter }
The service 'app.security.blog_post_voter' will be
automatically created.
The service container will be injected in
'app.security.blog_post_voter'.

Doctrine repositories

src
└── App
    └── Entity
        ├── BlogPost.php
        └── BlogPostRepository.php

services:
    blogpost_repository:
        class: "App\\Entity\\BlogPostRepository"
        factory_service: doctrine
        factory_method: getRepository
        arguments:
            - "App\\Entity\\BlogPost"
The service 'app.entity.blog_post_repository' will be
automatically created.

Form types

src
└── App
    └── Form
        ├── BlogPostType.php # implements Symfony\Component\DependencyInjection\ContainerAwareInterface
        └── EditBlogPostType.php

services:
    blogpost_type:
        class: "App\\Form\\BlogPostType"
        arguments: [@service_container]
        tags:
            - { name: form.type }

    editblogpost_type:
        class: "App\\Form\\EditBlogPostType"
        tags:
            - { name: form.type }
The services 'app.form.blog_post_type' and
'app.form.edit_blog_post_type' will be automatically created.
The service container will be injected in
'app.form.blog_post_type'.

Form type extensions

src
└── App
    └── Form
        └── Extension
            └── ImageTypeExtension.php # extends Symfony\Component\Form\AbstractTypeExtension

services:
    image_type_extension:
        class: "App\\Form\\Extension\\ImageTypeExtension"
        tags:
            - { name: form.type_extension, alias: file }
The services 'app.form.extension.image_type_extension'
will be automatically created.

Twig extensions

src
└── App
    └── Twig
        └── BlogPostExtension.php # implements Twig_ExtensionInterface

services:
    blogpost_extension:
        public: false
        class: "App\\Twig\\BlogPostExtension"
        tags:
            - { name: twig.extension }
The service 'app.twig.blog_post_extension' will be
automatically created.

Constraint validators

src
└── App
    └── Validator
        └── Constraints
            └── IsScalar.php # extends Symfony\Component\Validator\Constraint
            └── IsScalarValidator.php

services:
    app.constraints.validator.is_scalar_validator:
        class: "App\\Validator\\Constraints\\IsScalarValidator"
        tags:
            - { name: validator.constraint_validator, alias: "is_scalar" }
The service 'app.constraints.validator.is_scalar_validator'
will be automatically created.

Missing template creation

You have created an action, associated a route to it, but forgotten to create the template? Don't worry, the KnpRadBundle will provide you with a template creation assistant form!
Less switchs between your IDE and your browser means quicker development!

Flash message

<?php

// i18n-driven flash messages (actual)
// messages are in messages.yml and key is
// used in controllers - `app_blogposts_update.success`
public function updateAction()
{
    $this->get('session')
        ->getFlashBag()
        ->add('success', 'app_blogposts_update.success')
    ;
}

// entire message in controller
public function newAction()
{
    $this->get('session')
        ->getFlashBag()
        ->add('success', 'Successfully created')
    ;
}
<?php

// i18n-driven flash messages (actual)
// messages are in messages.yml and key is
// used in controllers - `app_blogposts_update.success`
public function updateAction()
{
    $this->addFlash('success');
}

// entire message in controller
public function newAction()
{
    $this->addFlash('success', 'Successfully created');
}

Mailer

<?php

public function sendAction()
{
    $message = \Swift_Message::newInstance()
        ->setSubject('Hello Email')
        ->setFrom('send@example.com')
        ->setTo('recipient@example.com')
        ->setBody(
            $this->renderView(
                'App:Mails:hello.txt.twig',
                array('name' => $name)
            )
        )
        ->addPart(
            $this->renderView(
                'App:Mails:hello.txt.twig',
                array('name' => $name)
            ),
            'text/html'
        )
        ->attach(\Swift_Attachment::fromPath('your-information.pdf'))
    ;

    $this->get('mailer')->send($message);
}
<?php

public function sendAction()
{
    $message = $this->createMessage(
        'hello',
        ['name' => $name],
        'send@example.com',
        'recipient@example.com'
    )->attach(\Swift_Attachment::fromPath('your-information.pdf'));

    $this->send($message);
}

IS_OWNER Voter

Permits to use the decision manager system to check if an object implementing `OwnableInterface` is owned by an object implementing `OwnerInterface`.

If object implementing `OwnerInterface` also implements `EquatableInterface` and `UserInterface`, it will be used to check equality of owner.

Otherwise, it will use object identity (===).

Imagine you have a `Post` class that is editable only by its owner:


<?php

use Knp\RadBundle\Security\OwnableInterface;

class Post implements OwnableInterface
{
    /**
     * @ORM\ManyToOne(targetEntity="User")
     **/
    public $createdBy;

    public function getOwner()
    {
        return $this->createdBy;
    }
}

And you have a `User` class that represents users in your system:


<?php

use Knp\RadBundle\Security\OwnerInterface;

class User implements OwnerInterface, UserInterface, EquatableInterface
{
}

When you want to know if an object is owned bu the current user, juste use:


<?php

$securityContext->isGranted('IS_OWNER', new Post); // true if Post::createdBy is current logged-in user


Combined with Resource Resolvers, you can be sure that an object is editable by the logged-in user even before any controller is reached.

Unsafe methods handling through link

Wiki

<!-- ... -->

<form
    action="{{ path('app_blogposts_delete', {'id': 1}) }}"
    onsubmit="return confirm('Are you sure?')"
    method="POST"
>
    <input type="hidden" name="_method" value="DELETE" />
    <input type="hidden" name="_token" value="{{ csrf_token('link') }}" />
    <input type="submit" value="Delete"/>
</form>

<!-- ... -->
<!-- ... -->

<a {{ link_attr('delete') }} href="{{ path('app_blogposts_delete', {'id': 1}) }}">Delete</a>

<!-- ... -->

<?php

public function deleteAction(Request $request, $id)
{
    $token = $request->request->get('_token');

    if (!$this->get('form.csrf_provider')->isCsrfTokenValid(
        'link',
        $token
    )) {
        throw new \InvalidArgumentException(
            'The CSRF token is invalid.' .
            'Please try to resubmit the form.'
        );
    }

    $em = $this->getDoctrine()->getManager();
    $blogPost = $em
        ->getRepository('App:Blogpost')
        ->find($id);

    if (null === $post) {
        throw $this->createNotFoundException(
            'Resource not found'
        );
    }

    $em->remove($blogPost);
    $em->flush();

    return $this->redirect(
        $this->generateUrl('app_blogposts_index')
    );
}
<?php

public function deleteAction($id)
{
    $blogPost = $this->findOr404('App:BlogPost', ['id' => $id]);
    $this->remove($blogPost, true);

    return $this->redirectToRoute('app_blogposts_index');
}

Shortcuts

Doctrine

<?php

$post = $this->getDoctrine()
    ->getManager()
    ->getRepository('App:BlogPost')
    ->find($id)
;

if (null === $post) {
    throw $this->createNotFoundException('Resource not found');
}
<?php

$post = $this->findOr404('App:BlogPost', ['id' => $id]);

Security

<?php

if (!$this->get('security.context')->isGranted(['POST_OWNER'], $post)) {
    throw new AccessDeniedHttpException('Access Denied');
}
<?php

if (!$this->isGranted(['POST_OWNER'], $post)) {
    throw $this->createAccessDeniedException();
}
Fork me on GitHub To Top