How to add TINYINT MySQL type to Doctrine in Symfony 2.8
Hello! A few days ago I needed to define an entity with a TINYINT type column to hold a rating value between 1 and 5. I was doing this in a Symfony 2.8 project and to my surprise, Doctrine was unable to handle this type of data out of the box (it uses it for booleans but it has no TINYINT standalone type).
So I searched the internet for a solution and I found some but nothing complete.
The solution (TINYINT vs Doctrine):
First you need to define the new Doctrine type class like this:
<?php namespace AppBundle\Types; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Platforms\AbstractPlatform; class TinyintType extends Type { const TINYINT = 'tinyint'; public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) { return 'TINYINT'; } public function convertToPHPValue($value, AbstractPlatform $platform) { return $value; } public function convertToDatabaseValue($value, AbstractPlatform $platform) { return $value; } public function getName() { return self::TINYINT; } }
Then, to register it, I used the boot function of the AppBundle:
<?php namespace AppBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; use Doctrine\DBAL\Types\Type; class AppBundle extends Bundle { public function boot() { parent::boot(); $em = $this->container->get('doctrine.orm.default_entity_manager'); // types registration if(!Type::hasType('tinyint')) { try { Type::addType('tinyint', 'AppBundle\Types\TinyintType'); $em->getConnection()->getDatabasePlatform()->registerDoctrineTypeMapping('TINYINT', 'tinyint'); } catch (\Exception $e) { } } } }
I had to use the try / catch block to avoid an error when running symfony console commands having no database defined:
PHP Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[42000] [1049] Unknown database 'rating'' in vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOConnection.php:43
Now you can use the new type in your entities:
/** * @ORM\Column(name="rating", type="tinyint") * @Assert\NotBlank() * @Assert\GreaterThanOrEqual( * value = 1 * ) * @Assert\LessThanOrEqual( * value = 5 * ) */ protected $rating;
Learn how to setup Docker machine for Symfony development.
Hope this helps someone :)
Have a great day!
Setup Testing and Fixtures in Symfony2: The Easy Way
Setting up testing and fixtures in Symfony2 is vital if you plan on starting Test Driven Development or you simply want to start covering your code with properly written tests that can access mock data.
1. Install PHPUnit and php 5.6
The first thing you need to do is to install PHPUnit on your machine:
$ wget https://phar.phpunit.de/phpunit.phar $ chmod +x phpunit.phar $ sudo mv phpunit.phar /usr/local/bin/phpunit $ phpunit --version
and then, if needed, also upgrade your PHP version to 5.6:
$ sudo apt-get install language-pack-en-base $ export LC_CTYPE="en_US.UTF-8" $ sudo add-apt-repository ppa:ondrej/php5-5.6 $ sudo apt-get update $ sudo apt-get install php5
and make sure everything's ok by running: phpunit -c app/
Please note that this is not a testing tutorial so if you'd like to learn more about how to actually test your Symfony2 app, then please read their documentation.
2. Setup and create a test database
In order to be able to configure testing and fixtures in a Symfony2 app a separate, independent database is needed so that your dev environment is not affected.
In the config_test.yml
file you simply need to add:
doctrine: dbal: host: 127.0.0.1 dbname: testdb user: [YOUR_MYSQL_USERNAME] password: [YOUR_MYSQL_PASSWORD]
then simply run php app/console doctrine:database:create --env=test
Related: Send Emails in Symfony2: The Right Way
3. Build the BaseTestSetup class
Once our test database is ready we can start building our BaseTestSetup
class, one which will serve as parent for all of our tests.
<?php namespace AppBundle\Tests; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; abstract class BaseTestSetup extends WebTestCase { protected $client; protected $container; protected $em; protected function setUp() { $this->client = static::createClient(); $this->container = $this->client->getContainer(); $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); } }
4. Install the LiipFunctionalTestBundle
Even though Symfony does have an out of the box solution to setup and test your app, the LiipFunctionalTestBundle provides base classes for functional tests to assist in setting up testing and fixtures and HTML5 validation.
After you install and configure the bundle, go back to your BaseTestSetup
class and make the necessary modifications:
<?php namespace AppBundle\Tests; use Doctrine\ORM\Tools\SchemaTool; use Liip\FunctionalTestBundle\Test\WebTestCase; abstract class BaseTestSetup extends WebTestCase { protected $client; protected $container; protected $em; protected function setUp() { $this->client = static::createClient(); $this->container = $this->client->getContainer(); $this->em = static::$kernel->getContainer() ->get('doctrine') ->getManager(); if (!isset($metadatas)) { $metadatas = $this->em->getMetadataFactory()->getAllMetadata(); } $schemaTool = new SchemaTool($this->em); $schemaTool->dropDatabase(); if (!empty($metadatas)) { $schemaTool->createSchema($metadatas); } $this->postFixtureSetup(); $this->loadFixtures(array( 'AppBundle\DataFixtures\ORM\LoadUserData', )); } }
The new code above simply drops and creates the database each them tests run. Then it loads the fixtures that you'll need. The LoadUserData
class does not exist yet so we'll go right ahead and add it by following the handy tutorial found on Symfony's website.
5. Write the very first test
Now that have your fixtures ready, you can go ahead and write your first test. Create a new file in your AppBundle/Tests
folder called UserTest.php
, next to BaseTestSetup.php
:
<?php namespace AppBundle\Tests; use AppBundle\Tests\BaseTestSetup; class UserTest extends BaseTestSetup { public function testSuggestImprovementEmail() { // assuming that you named your user 'username' in your Fixtures $crawler = $this->client->request('GET', '/show/username'); $this->assertGreaterThan( 0, $crawler->filter('html:contains("Hello first user!")')->count() ); } }
And that's about it! You now have everything you need to properly test your awesome app.
How to monitor Symfony and Apache logs with M/Monit
So you have developed and deployed a fresh new project and you want to be alerted in case an error happens.
Here's how you do it using the M/Monit tool.
It can be installed on an Ubuntu machine using the following command:
sudo apt-get install monit
Now edit the /etc/monit/monitrc
configuration file and add the following:
set mailserver smtp.sendgrid.net port 587 username "USERNAME" password "PASSWORD" set alert YOUR_EMAIL@HOST.COM not on { instance, action } set httpd port 2812 and use address localhost # only accept connection from localhost allow localhost # allow localhost to connect to the server and allow admin:monit # require user 'admin' with password 'monit' check file error.log with path /var/log/apache2/error.log if match "error" then alert check file prod.log with path /var/www/sfproject/app/logs/prod.log if match "request.CRITICAL" then alert
First we need to tell M/Monit what mail server to use (we used Sendgrid but you can use whatever you want), then to specify the email address which will receive the alerts. We also have to allow connections from localhost so the status
command below can be executed.
The actual monitoring is set up in the last two blocks of code, one for the apache error log and the next for the Symfony production logs. Anytime the specified string will appear in the logs, an alert email will be sent to the address we defined above.
To enable the changes, reload configuration:
monit reload
You can now check the status with the command:
monit status
It's that simple!
Send Emails in Symfony2: The Right Way
No matter what your app is about, at one you point you will have to send emails in Symfony2. Even though this process might seem straightforward and easy to apply, it can actually get pretty tricky. Here's how you can send emails in Symfony2 by properly splitting your functionality and templates.
1. Mailer parameters
First off, you're going to need to add some email data into your parameters.yml
file that will tell Swift Mailer which SMTP server to use. At this point, it would be advised to create an account at an email sending service. For this short tutorial, we're going to use SendGrid.
mailer_transport: smtp mailer_host: smtp.sendgrid.net mailer_user: your_sendgrid_username mailer_password: 'your_sendgrid_password' mailer_port: 587 contact_email: contact@yourapp.com from_email: hello@yourapp.com from_name: YourApp
As you can see, we've also added three parameters that will help us to send emails in Symfony2: contact email, from email and from name.
2. Send emails in Symfony2 using a MailManager
Having a MailManager is helpful because the code needed to send emails will only reside in a single place. Thus, it will be way easier to maintain. Here's how you should build the MailManager class:
<?php namespace AppBundle\Lib; class MailManager { protected $mailer; protected $twig; public function __construct(\Swift_Mailer $mailer, \Twig_Environment $twig) { $this->mailer = $mailer; $this->twig = $twig; } /** * Send email * * @param string $template email template * @param mixed $parameters custom params for template * @param string $to to email address or array of email addresses * @param string $from from email address * @param string $fromName from name * * @return boolean send status */ public function sendEmail($template, $parameters, $to, $from, $fromName = null) { $template = $this->twig->loadTemplate('AppBundle:Mail:' . $template . '.html.twig'); $subject = $template->renderBlock('subject', $parameters); $bodyHtml = $template->renderBlock('body_html', $parameters); $bodyText = $template->renderBlock('body_text', $parameters); try { $message = \Swift_Message::newInstance() ->setSubject($subject) ->setFrom($from, $fromName) ->setTo($to) ->setBody($bodyHtml, 'text/html') ->addPart($bodyText, 'text/plain') ; $response = $this->mailer->send($message); } catch (\Exception $ex) { return $ex->getMessage(); } return $response; } }
In the sendEmail
function, you can easily define a $template
variable which will hold whatever template you need, depending on the type of email. You'll see the MailManager
in action in the 4th and last section of this short tutorial, where you'll use it to send a contact email.
Oh, and don't forget to register the service in services.yml
:
mail_manager: class: AppBundle\Lib\MailManager arguments: ["@mailer", "@twig"]
Related: Symfony2 Facebook and Google Login: The Easy Way
3. Define a simple template
In this section you'll be building the three main blocks any email should have (subject, html and text) and store them in a template which you'll extend as needed.
{# src/AppBundle/Mail/template.html.twig #} {% block subject %} default subject {% endblock %} {% block body_text %} default text {% endblock %} {% block body_html %} <p>default body</p> {% endblock %}
4. A sample email sending action
Now that you have everything set, you can go ahead and build your first mail sending action. As an example, here's how a contact action should look like. It will send emails to the email address defined in parameters.yml
.
public function contactAction(Request $request) { $form = $this->createForm(new ContactType()); $form->handleRequest($request); //this is where we define which template to use $template = 'contact'; if($form->isValid()){ //Get data from the submitted form $data = $form->getData(); $mail_params = array( 'firstName' => $data["firstName"], 'lastName' => $data["lastName"], 'message' => $data["message"], 'phoneNumber' => $data["phoneNumber"], 'email' => $data["email"] ); //grab the addresses defined in parameters.yml $to = $this->container->getParameter('contact_email'); $from = $this->container->getParameter('from_email'); $fromName = $this->container->getParameter('from_name'); //use the MailManager service to send emails $message = $this->container->get('mail_manager'); $message->sendEmail($template, $mail_params, $to, $from, $fromName); return $this->redirectToRoute('contact'); } return $this->render('AppBundle:StaticPages:contact.html.twig',array( 'form' => $form->createView() )); }
Since you'll be using the contact
email body, you will need to build it by extending the template defined at step #3:
{% extends "AppBundle:Mail:template.html.twig" %} {% block subject %} YourApp Contact Message {% endblock %} {% block body_text %} {% autoescape false %} Name: {{ firstName }} {{ lastName }} Message: {{ message }} Phone Number: {{ phoneNumber }} Email address: {{ email }} {% endautoescape %} {% endblock %} {% block body_html %} {% autoescape false %} <p>Name: {{ firstName }} {{ lastName }}</p> <p>Message: {{ message }}</p> <p>Phone Number: {{ phoneNumber }}</p> <p>Email address: {{ email }}</p> {% endautoescape %} {% endblock %}
And there you have it - an easy way to build and maintain your emails. If you know of any other easier system used to send emails in Symfony2, please let me know in the comment section below.
Getting Started with Building APIs in Symfony2
Hello all you Interwebs friends! While we're passing through the shallow mists of time, REST is becoming more and more of a universal standard when building web applications. That said, here's a very brief tutorial on how to get started with building APIs in Symfony2.
Spoiler alert: the bits of code written below use FosUserBundle + Doctrine.
1. Generate a New Bundle
It's nice to keep your code neat and tidy, so ideally, you should create a separate bundle that will only store your API code, we can generically name ApiBundle.
$ php app/console generate:bundle --bundle-name=ApiBundle --format=annotation
2. Versioning
This isn't by any means mandatory, but if you believe that your API endpoints will suffer major changes along the way, that cannot be predicted from the get go, it would be nice to version your code. This means that you would, initially, have a prefix in your endpoints, like: `/v1`/endpoint.json, and you'd increase that value each time a new version comes along. I'll describe how to actually create the first version (`v1`) of your API a little further down the line.
3. Install a Few Useful Bundles
- FosRestBundle - this bundle will make our REST implementation a lot easier.
$ composer require friendsofsymfony/rest-bundle
and then include FOSRestBundle in your `AppKernel.php` file:
$bundles = array( // ... new FOS\RestBundle\FOSRestBundle(), );
- JmsSerializerBundle - this will take care of the representation of our resources, converting objects into JSON.
composer require jms/serializer-bundle
and then include JMSSerializerBundle in your `AppKernel.php`:
$bundles = array( // ... new JMS\SerializerBundle\JMSSerializerBundle(), // ... );
4. Configurations
Configure the Response object to return JSON, as well as set a default format for our API calls. This can be achieved by adding the following code in `app/config/config.yml`:
fos_rest: format_listener: rules: - { path: ^/api/, priorities: [ html, json, xml ], fallback_format: ~, prefer_extension: true } routing_loader: default_format: json param_fetcher_listener: true view: view_response_listener: 'force' formats: xml: true json: true templating_formats: html: true
5. Routing
I prefer using annotations as far as routes go, so all we need to do in this scenario would be to modify our API Controllers registration in `/app/config/routing.yml`. This registration should have already been created when you ran the generate bundle command. Now we'll only need to add our version to that registration. As far as the actual routes of each endpoint go, we'll be manually defining them later on, in each action's annotation.
api: resource: "@ApiBundle/Controller/V1/" type: annotation prefix: /api/v1
At this point we're all set to start writing our first bit of code. First off, in our Controller namespace, we would want to create a new directory, called `V1`. This will hold all of our v1 API Controllers. Whenever we want to create a new version, we'll start from scratch by creating a `V2` namespace and so on.
After that's done let's create an action that will GET a user (assuming we've previously created a User entity and populated it with users). This would look something like:
<?php namespace ApiBundle\Controller\V1; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Controller\Annotations as Rest; class UserController extends FOSRestController { /** * @return array * @Rest\Get("/users/{id}") * @Rest\View */ public function getUserAction($id) { $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('AppBundle:User')->find($id); return array('user' => $user); } }
If we want to GET a list of all users, that's pretty straightforward as well:
/** * GET Route annotation. * @return array * @Rest\Get("/users/get.{_format}") * @Rest\View */ public function getUsersAction() { $em = $this->getDoctrine()->getManager(); $users = $em->getRepository('AppBundle:User')->findAll(); return array('users' => $users); }
With that done, when running a GET request on `https://yourapp.com/api/v1/users/1.json`, you should get a `json` response with that specific user object.
What about a POST request? Glad you asked! There are actually quite a few options to do that. One would be to get the request data yourself, validate it and create the new resource yourself. Another (simpler) option would be to use Symfony Forms which would handle all this for us.
The scenario here would be for us to add a new user resource into our database.
If you're also using FosUserBundle to manage your users, you can just use a similar RegistrationFormType:
<?php namespace ApiBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; class RegistrationFormType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('email', 'email') ->add('username') ->add('plainPassword', 'password') ; } public function configureOptions(OptionsResolver $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\User', 'csrf_protection' => false )); } public function getName() { return 'my_awesome_form'; } }
Next, we'll want to actually create our addUserAction():
/** * POST Route annotation. * @Rest\Post("/users/new.{_format}") * @Rest\View * @return array */ public function addUserAction(Request $request) { $userManager = $this->container->get('fos_user.user_manager'); $user = $userManager->createUser(); $form = $this->createForm(new \ApiBundle\Form\Type\RegistrationFormType(), $user); $form->handleRequest($request); if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->flush(); return array('user' => $user); } return View::create($form, 400); }
To make a request, you'll need to send the data as raw JSON, to our `https://yourapp.com/api/v1/users/new.json` POST endpoint:
{ "my_awesome_form":{ "email":"andrei@ibw.com", "username":"sdsa", "plainPassword":"asd" } }
And that's all there is to it.
We haven't covered the cases where you'd want Delete or Update a user yet. Updating resources through the REST standards can be done using either PUT or PATCH. The difference between them is that PUT will completely replace your resource, while PATCH will only, well, patch it... meaning that it will partially update your resource with the input it got from the API request.
Let's get to it then. We'll use the same form as before, and we'll try to only change (we'll use the PATCH verb for that) the email address, username and password of our previously created user:
/** * PATCH Route annotation. * @Rest\Patch("/users/edit/{id}.{_format}") * @Rest\View * @return array */ public function editAction(Request $request, $id) { $userManager = $this->container->get('fos_user.user_manager'); $user = $userManager->findUserBy(array('id'=>$id)); $form = $this->createForm(new \ApiBundle\Form\Type\RegistrationFormType(), $user, array('method' => 'PATCH')); $form->handleRequest($request); if ($user) { if ($form->isValid()) { $em = $this->getDoctrine()->getManager(); $em->persist($user); $em->flush(); return array('user' => $user); } else { return View::create($form, 400); } } else { throw $this->createNotFoundException('User not found!'); } }
The request body is the same as the one shown for the POST method, above. There are a few small differences in our edit action though. First off - we're telling our form to use the PATCH method. Second - we are handling the case where the user ID provided isn't found.
The Delete method is the easiest one yet. All we need to do is to find the user and remove it from our database. If no user is found, we'll throw a "user not found" error:
/** * DELETE Route annotation. * @Rest\Delete("/users/delete/{id}.{_format}") * @Rest\View(statusCode=204) * @return array */ public function deleteAction($id) { $em = $this->getDoctrine()->getManager(); $user = $em->getRepository('AppBundle:User')->find($id); $em->remove($user); $em->flush(); }
Related: Routing in Symfony2
Conclusions
Aaand we're done. We now have a fully working CRUD for our User Entity. Thanks for reading and please do share your opinions/suggestions in the comment section below.
Symfony2 Doctrine Migrations with Unique Indexes (Slugs)
This is something me and my colleagues encounter from time to time.
The Problem
One of the problems of adding unique indexes to existing data, like adding the sluggable Doctrine behaviour using the StofDoctrineExtensionsBundle, is that the generated migration will end up throwing an error:
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '' for key 'UNIQ_BDAFD8C8989D9B62'
Causes
Since the default values for the new MySQL column are not unique, adding the unique index is not possible - which is what the error above is telling us. So we will need to change the migration to also generate the unique values before adding the index.
Solution
In order to do so, we will have to split the generated migration into 2 different migrations, one for adding the new column, which could be a slug, and the other to add the unique index. After running the first migration, we need to execute the code that generates the unique values needed for the index. We can use the postUp
method in a Doctrine migration to execute code after the “up” migration finished. We will also need to instantiate and boot the kernel in order to gain access to the Symfony framework and build our functionality like we would do in a controller:
<?php namespace Application\Migrations; use Doctrine\DBAL\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; /** * Auto-generated Migration: Please modify to your needs! */ class Version20151021133750 extends AbstractMigration { protected static $class = 'AppKernel'; protected static $kernel; /** * Creates a Kernel. * * Available options: * * * environment * * debug * * @param array $options An array of options * * @return HttpKernelInterface A HttpKernelInterface instance */ protected static function createKernel(array $options = array()) { if (null === static::$class) { static::$class = static::getKernelClass(); } return new static::$class( isset($options['environment']) ? $options['environment'] : 'test', isset($options['debug']) ? $options['debug'] : true ); } /** * Creates a Client. * * @param array $options An array of options to pass to the createKernel class * @param array $server An array of server parameters * * @return Client A Client instance */ protected static function createClient(array $options = array(), array $server = array()) { if (null !== static::$kernel) { static::$kernel->shutdown(); } static::$kernel = static::createKernel($options); static::$kernel->boot(); $client = static::$kernel->getContainer()->get('test.client'); $client->setServerParameters($server); return $client; } /** * @param Schema $schema */ public function up(Schema $schema) { // this up() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE book ADD slug VARCHAR(128) DEFAULT ""'); //$this->addSql('CREATE UNIQUE INDEX UNIQ_CBE5A331989D9B62 ON book (slug)'); } public function postUp(Schema $schema) { $this->client = self::createClient(); $this->em = $this->client->getKernel()->getContainer()->get('doctrine')->getEntityManager(); $books = $this->em->getRepository('AppBundle:Book')->findAll(); foreach($books as $book){ // need this so we force the generation of a new slug $book->setSlug(null); $this->em->persist($book); } $this->em->flush(); } /** * @param Schema $schema */ public function down(Schema $schema) { // this down() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); //$this->addSql('DROP INDEX UNIQ_CBE5A331989D9B62 ON book'); $this->addSql('ALTER TABLE book DROP slug'); } }
For the second migration file we only add the code necessary to add/remove the unique indexes:
<?php namespace Application\Migrations; use Doctrine\DBAL\Migrations\AbstractMigration; use Doctrine\DBAL\Schema\Schema; /** * Auto-generated Migration: Please modify to your needs! */ class Version20151021141028 extends AbstractMigration { /** * @param Schema $schema */ public function up(Schema $schema) { // this up() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('ALTER TABLE book CHANGE slug slug VARCHAR(128) NOT NULL'); $this->addSql('CREATE UNIQUE INDEX UNIQ_CBE5A331989D9B62 ON book (slug)'); } /** * @param Schema $schema */ public function down(Schema $schema) { // this down() migration is auto-generated, please modify it to your needs $this->abortIf($this->connection->getDatabasePlatform()->getName() != 'mysql', 'Migration can only be executed safely on \'mysql\'.'); $this->addSql('DROP INDEX UNIQ_CBE5A331989D9B62 ON book'); $this->addSql('ALTER TABLE book CHANGE slug slug VARCHAR(128) DEFAULT \'\' COLLATE utf8_unicode_ci'); } }
You can generate an empty migration file using the doctrine:migratios:generate
command.
If you now run the doctrine:migrations:migrate
command everything should be fine and the database should be populated with the unique values we needed in the first place.
Conclusion
Luckily I solved this issue before an important deadline. Let me know if you found any other way around it, or a quicker solution to this issue.
How to Fix Symfony2 Ajax Login Redirect
You probably noticed that sometimes an Ajax request will return the login page instead of the actual content is should return. This happens when the user has beed logged out in the background and the current page does not reflect that (it could happen if the session expired or if the user simply logged out from another browser window/tab).
How to Fix Symfony2 Ajax Login Redirect
Here's a quick way to fix this: we will create an event listener that will catch this authentication exception, check for an Ajax request and, if found, it will return a 403 http code instead of redirecting to the login page. The JavaScript code will then know to reload the page and thus redirect to login in case of 403 instead of loading and showing the received content to the user.
Here's the Symfony2 event listener:
<?php // src/AppBundle/EventListener/AjaxAuthenticationListener.php namespace AppBundle\EventListener; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent; /** */ class AjaxAuthenticationListener { /** * Handles security related exceptions. * * @param GetResponseForExceptionEvent $event An GetResponseForExceptionEvent instance */ public function onCoreException(GetResponseForExceptionEvent $event) { $exception = $event->getException(); $request = $event->getRequest(); if ($request->isXmlHttpRequest()) { if ($exception instanceof AuthenticationException || $exception instanceof AccessDeniedException) { $event->setResponse(new Response('', 403)); } } } }
As always, we will have to register it as a service:
services: ajax.authentication.listener: class: AppBundle\EventListener\AjaxAuthenticationListener tags: - { name: kernel.event_listener, event: kernel.exception, method: onCoreException, priority: 1000 }
How to Fix Symfony2 Ajax Login Redirect
In the JavaScript code we add the following to make jQuery treat the Ajax errors by reloading the window in case of a 403 error. What will actually happen is that the user will end on the login page as he is no longer authenticated.
$(document).ready(function() { $(document).ajaxError(function (event, jqXHR) { if (403 === jqXHR.status) { window.location.reload(); } }); });
How To Enable Email Confirmation On Fosuserbundle Profile Edit
We all know and use FOSUserBundle in our Symfony applications, so much it became kind of a standard. It provides everything you need for user management: login, registration, email confirmation and much more control over the access of the user in your application. But we found a thing missing from this awesome package: email confirmation after the initial email address has been changed through a profile edit. In the following lines we will show you how to extend the FOSUserBundle to implement this.
How To Enable Email Confirmation On Fosuserbundle Profile Edit
This post assumes you are familiar (even advanced) with the Symfony framework and FOSUserBundle.
To get started we will need a listener to be triggered when a profile edit has happened, FOSUserBundle fires two events that we are interested in: FOSUserEvents::PROFILE_EDIT_INITIALIZE
and FOSUserEvents::PROFILE_EDIT_SUCCESS
. The first one is triggered before the actual profile data is changed so we will use that to get a hold on the original email address. When the second event is fired, we will compare the initial email address with the current one and, if they are not the same, we will start the confirmation process:
<?php // src/AppBundle/EventListener/ProfileEditListener.php namespace AppBundle\EventListener; use FOS\UserBundle\FOSUserEvents; use FOS\UserBundle\Event\FormEvent; use FOS\UserBundle\Event\GetResponseUserEvent; use FOS\UserBundle\Mailer\MailerInterface; use FOS\UserBundle\Util\TokenGeneratorInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\HttpFoundation\Session\SessionInterface; class ProfileEditListener implements EventSubscriberInterface { private $oldEmail; private $mailer; private $tokenGenerator; private $router; private $session; private $tokenStorage; public function __construct(MailerInterface $mailer, TokenGeneratorInterface $tokenGenerator, UrlGeneratorInterface $router, SessionInterface $session, TokenStorageInterface $tokenStorage) { $this->mailer = $mailer; $this->tokenGenerator = $tokenGenerator; $this->router = $router; $this->session = $session; $this->tokenStorage = $tokenStorage; } public static function getSubscribedEvents() { return array( FOSUserEvents::PROFILE_EDIT_INITIALIZE => 'onProfileEditInitialize', FOSUserEvents::PROFILE_EDIT_SUCCESS => 'onProfileEditSuccess' ); } public function onProfileEditInitialize(GetResponseUserEvent $event) { $this->oldEmail = $event->getUser()->getEmail(); } public function onProfileEditSuccess(FormEvent $event) { $user = $event->getForm()->getData(); if ($user->getEmail() !== $this->oldEmail) { // disable user $user->setEnabled(false); // send confirmation token to new email $user->setConfirmationToken($this->tokenGenerator->generateToken()); $this->mailer->sendConfirmationEmailMessage($user); // force user to log-out $this->tokenStorage->setToken(); // redirect user to check email page $this->session->set('fos_user_send_confirmation_email/email', $user->getEmail()); $url = $this->router->generate('fos_user_registration_check_email'); $event->setResponse(new RedirectResponse($url)); } } }
Now, add this to your services.yml
file and you're good to go:
app.profile_edit_listener: class: AppBundle\EventListener\ProfileEditListener arguments: [@fos_user.mailer, @fos_user.util.token_generator, @router, @session, @security.token_storage] tags: - { name: kernel.event_subscriber }
How To Enable Email Confirmation On Fosuserbundle Profile Edit
One last thing: you will probably want to change the email template that is sent to the user with the confirmation link. You can overwrite it by creating app/Resources/FOSUserBundle/views/Registration/email.txt.twig
and put what you need in there (use the original one from vendor/friendsofsymfony/user-bundle/Resources/views/Registration/email.txt.twig
to see how to get the confirmation link).
Symfony2: Doctrine 2 Entity Listeners
When working on a little more complex application is inevitable that we'll get to the point when we need to trigger an action when something happens somewhere in our application. Quite often is the case that we need to bind our actions to an entity, like for example notify all subscribers to a blog post that the post they are following has changed.
How to Add Share Buttons to Symfony2 Generated Web Pages
Some projects require you to add social share buttons for their pages. I'm gonna show you a simple way to do this using the Socialitejs JavaScript library.
First you're gonna have to download the Socialitejs library, save it to your bundle Resources/public/js
folder and publish the assets.
If you have a general layout that is inherited by all your specific page templates (like it's usually the case), just add the share code in it like so:Read more