KNP Labs RAD Components

Rapid Application development with Symfony

Why ?

Bring enhanced development experience to your Symfony projects and get rid of these tiresome tasks you feel doing over and over. Spend your time elsewhere!

Fast

Straight to the point usage.

No hidden complexity. Fast results.

Easy to use

Minimal configuration.

Components are plug & play.

Non intrusive

Does not pollute your code. Breaks nothing.

Use it, or not :)

Atomic / Lightweight

Each component is simple and stupid.

It does one thing and it does it well.

Tested

It is so green.

Elegant

Follows the SOLID principles.

PSR compliant.

Resource Resolver

Resolve resources from the routing

Resource Resolver is a way to resolve request attributes and get directly the value. It is very similar to Param Converters, but however far more flexible and powerful.

# routing.yml

article_index:
    path: /articles/
    defaults:
        # ...

article_show:
    path: /articles/{id}
    defaults:
        # ...
<?php

namespace App\Controller;

class ArticlesController
{
    public function indexAction()
    {
        $articles = $this->getDoctrine()
            ->getManager()
            ->getRepository('App:Article')
            ->findAll()
        ;

        // ...
    }

    public function showAction($articleId)
    {
        $article = $this->getDoctrine()
            ->getManager()
            ->getRepository('App:Article')
            ->find($articleId)
        ;

        // ...
    }
}
# routing.yml

article_index:
    path: /articles/
    defaults:
        # ...
        _resources:
            articles:
                service: my.article.repository
                method: findAll

article_show:
    path: /articles/{id}
    defaults:
        # ...
        _resources:
            article:
                service: my.article.repository
                method: find
                arguments: [$id]
<?php

namespace App\Controller;

use App\Entity\Article;

class ArticlesController
{
    public function indexAction(array $articles)
    {
        // ...
    }

    public function showAction(Article $article)
    {
        // ...
    }
}

Security

Perform security check at routing level instead of in the controller.

Through simple configuration in the routing, this component allows to grant access according resolved resources. And combined with the rad-resource-resolver component, it also allows to set conditions on request attributes.

<?php

namespace App\Controller;

class ArticleController extends Controller
{
    public function indexAction()
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY', null, 'Unable to access this page!');

        // ...
    }

    public function detailsAction($articleId)
    {
        $article = $this->getDoctrine()
            ->getManager()
            ->getRepository('App:Article')
            ->find($articleId)
        ;

        $owners = $this->getDoctrine()
            ->getManager()
            ->getRepository('App:Owner')
            ->findAllForArticle($article)
        ;

        if (!$this->isGranted(['IS_MEMBER', 'ANOTHER_ROLE'], $article) ||
            !$this->isGranted('IS_ADMIN', $owners)
        ) {
            throw $this->createAccessDeniedException();
        }

        // ...
    }
}
# routing.yml
article_index:
    path: /articles/
    defaults:
        // ...
        _security:
            - roles: IS_AUTHENTICATED_FULLY

article_details:
    path: /articles/{id}/owners/
    defaults:
        // ...
        _resources:
            article:
                // ...
            owners:
                // ...
        _security:
            -
                roles: [IS_MEMBER, ANOTHER_ROLE]
                subject: article
            -
                roles: IS_ADMIN
                subject: owners

Doctrine Events

Doctrine to Symfony events redispatcher

It is able to access to your doctrine events from Symfony DependencyInjection component easily.

services:
    app.event_listener.some_entity_listener:
        class: App\EventListener\SomeEntityListener
        tags:
            - { name: doctrine.event_listener, event: pre_persist, method: prePersist }
<?php

namespace App\EventListener;

use App\Entity\SomeEntity;
use Doctrine\ORM\Event\LifecycleEventArgs;

class SomeEntityListener
{
    public function prePersist(LifecycleEventArgs $args)
    {
        $entity = $args->getEntity();

        if (false === $entity instanceof SomeEntity) {
            return;
        }

        // Some stuff
    }
}
services:
    app.event_listener.some_entity_listener:
        class: App\EventListener\SomeEntityListener
        tags:
            - { name: kernel.event_listener, event: app.entity.some_entity.pre_persist, method: prePersist }
<?php

namespace App\EventListener;

use Knp\Rad\DoctrineEvent\Event\DoctrineEvent;

class SomeEntityListener
{
    public function prePersist(DoctrineEvent $event)
    {
        $entity = $event->getEntity();

        // Some stuff
    }
}

Auto Registration

Forget about those many common services declarations

Don't bother anymore registrate your repositories, form extensions, security voters or twig extensions as services... Just small configuration, stored files under specific sub-namespace, and all repositories, extensions and voters are auto-registred.

# services.yml

# Hurtful declarations...

app.form.extension.my_form_extension:
    class: App\Form\Extension\MyFormExtension
    tags:
        - { name: form.type_extension, extended_type: SomeType }

app.repository.some_entity_repository:
    class: App\Repository\SomeEntityRepository
    factory_service: doctrine.orm.default_entity_manager
    factory_method: getRepository
    arguments:
        - App\Entity\SomeEntityRepository

app.security.some_security_voter
    class: App\Security\SomeSecurityVoter
    tags:
        - { name: security.voter }

app.twig.some_extension:
    class: App\Twig\SomeExtension
    tags:
        - { name: twig.extension }
# config.yml

# Just activate the lines you need

knp_rad_auto_registration:
    enable:
        doctrine: ~
        doctrine_mongodb: ~
        doctrine_couchdb: ~
        form_type_extension: ~
        security_voter: ~
        twig_extension: ~

Fixtures Load

Allows you to use nelmio/alice library when loading your fixtures.

<?php

namespace App\DataFixtures\ORM;

use Doctrine\Common\DataFixtures\FixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use App\Entity\User;

class LoadUserData implements FixtureInterface
{
    public function load(ObjectManager $manager)
    {
        for ($i = 0; $i < 10; $i++) {
            $timestamp = mt_rand(1, time());
            $randomDate = new DateTime('@'.$timestamp);

            $user = new User();
            $user->setUsername($this->generateUsername());
            $user->setBirthday($randomDate);
            $user->setEmail($this->generateEmail());

            $manager->persist($user);
        }

        $manager->flush();
    }

    private function generateUsername()
    {
        $usernames = ['artium', 'nondum', 'codicem', 'primae', 'firmato'];

        return array_rand($usernames);
    }

    private function generateEmail()
    {
        $mails = ['mittendus@mail.com', 'fumo@mail.com', 'lanuginis@mail.com', 'spectante@mail.com', 'duci@mail.com'];

        return array_rand($mails);
    }
}
#Symfony2
$ php app/console doctrine:fixtures:load

#Symfony3
$ php bin/console doctrine:fixtures:load
# Resources/fixtures/orm/users.yml

App\Entity\User:
    user_{1..10}:
        username: <username()>
        birthday: <dateTime()>
        email: <email()>
#Symfony2
$ php app/console rad:fixtures:load

#Symfony3
$ php bin/console rad:fixtures:load

Users

A simple way to handle password encryption and salt generation.

Before your entity is inserted or updated into your database, according traits you choose to use, the salt, plain password or password will be automaticly generated. And all of this is done through three interfaces and three listeners.

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;

class User implements UserInterface
{
    private $id;

    private $password;

    // ...

    public function __construct()
    {
        $this->salt = md5(uniqid(null, true));
    }

    // ...

    public function setPassword($password)
    {
        $this->password = $password;

        return $this;
    }

    public function getSalt()
    {
        return $this->salt;
    }

    public function eraseCredentials()
    {
    }
}
<?php

namespace App\Controller;

use App\Controller\Controller;
use App\Entity\User;

class UserController extends Controller
{
    public function newUserAction($email,$plainPassword)
    {
        $user = new User();
        $salt = $user->getSalt();

        $factory = $this->container->get('security.encoder_factory');
        $encoder = $factory->getEncoder($user);
        $password = $encoder->encodePassword($plainPassword, $salt);

        $user->setPassword($password);
        $user->setEmail($email);

        // ...
    }
}
<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Knp\Rad\User\HasPassword;
use Knp\Rad\User\HasSalt;

class User implements HasPassword, HasSalt
{
    use HasPassword\HasPassword;
    use HasSalt\HasSalt;

    private $id;

    private $password;

    private $salt;

    // ...
}
<?php

namespace App\Controller;

use App\Controller\Controller;
use App\Entity\User;

class UserController extends Controller
{
    public function newUserAction($email,$plainPassword)
    {
        $user = new User();
        $user->setPlainPassword($plainPassword);
        $user->setEmail($email);

        // ...
    }
}

View Renderer

Don't bother with render functions anymore.

Following the Symfony conventions, the View Renderer component will try to render the appropriate twig template, if no Response object was returned from the Controller. You can still of course return a response when you want.

<?php

namespace App\Controller;

class BasicController
{
    public function someAction($var)
    {
        return $this->render(
            'App:Basic:some.html.twig',
            ['var' => $var]
        );
    }
}
<?php

namespace App\Controller;

class BasicController
{
    public function someAction($var)
    {
        // Will look for App\Resources\views\Basic\some.html.twig
        return ['var' => $var];
    }
}

Domain Event

A lightweight domain event pattern implementation for Doctrine2

The raise method allows you to trigger any event in your entity which will be transformed to a Knp\Rad\DomainEvent\Event object and dispatched once the entity has been flushed.

<?php

namespace App\Entity;

class MyEntity
{
    private $param;

    public function setParam($param)
    {
        $this->param = $param;

        return $this;
    }
}
<?php

namespace App\EventListener;

use Doctrine\ORM\Event\PreUpdateEventArgs;
use App\Entity\MyEntity;

class MyEventListener
{
    public function preUpdate(PreUpdateEventArgs $event)
    {
        $entity = $event->getEntity();

        if (!$entity instanceof MyEntity) {
            return;
        }

        if ($event->hasChangedField('param')) {
            // Some stuff
        }
    }
}
# services.yml

app.event_listener.my_event_listener:
    class: AppBundle\EventListener\MyEventListener
    tags:
        - { name: doctrine.event_listener, event: preUpdate }
<?php

namespace App\Entity;

use Knp\Rad\DomainEvent;

class MyEntity implements DomainEvent\Provider
{
    use DomainEvent\ProviderTrait;

    private $param;

    public function setParam($param)
    {
        $this->param = $param;

        $this->raise('myEventName', ['anyKey' => 'anyValue']);

        return $this;
    }
}
<?php

namespace App\EventListener;

use Knp\Rad\DomainEvent\Event;

class MyEventListener
{
    public function onMyEventName(Event $event)
    {
        $anyValue = $event->anykey;

        // Some stuff
    }
}
# services.yml

app.event_listener.my_event_listener:
    class: App\EventListener\MyEventListener
    tags:
        - { name: kernel.event_listener, event: myEventName, method: 'onMyEventName' }

Url Generation

This component will simply auto-complete needed route parameters with existing ones.

Just continue to use former url generation, nothing changes concerning the implementation. The only change is you don't repeat current route parameters anymore.

<?php

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class TestController
{
    /**
     * @Route("/shop/{shopId}", name="shop_show")
     */
    public function showAction($shopId)
    {
        return $this->redirectToRoute('shop_products', [
            'shopId' => $shopId,
        ]);
    }

    /**
     * @Route("/shop/{shopId}/products", name="shop_products")
     */
    public function productsAction($shopId)
    {
        // ...
    }
}
<?php

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

class TestController
{
    /**
     * @Route("/shop/{shopId}", name="shop_show")
     */
    public function showAction($shopId)
    {
        return $this->redirectToRoute('shop_products');
    }

    /**
     * @Route("/shop/{shopId}/products", name="shop_products")
     */
    public function productsAction($shopId)
    {
        // ...
    }
}

Works also with url and path Twig functions or everywhere else you use the Symfony Router.