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.
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!
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
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: /
# 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']);
}
}
<?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()];
}
}
<?php
public function newAction(Request $request)
{
$post = new BlogPost();
$form = $this->createForm(new NewBlogPostType(), $post);
$form->handleRequest($request)
if (!$request->isMethodSafe() && $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()];
}
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"]}
<?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];
}
}
<?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'
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.
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'.
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.
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'.
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.
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.
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.
<?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');
}
<?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);
}
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
<!-- ... -->
<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');
}
<?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]);
<?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();
}