Thursday, July 21, 2016

Using the Symfony Validator component as a standalone library

Sometimes I need to build some scripts without using a full framework like Symfony. The good news is that you can use individual components from Symfony. Most often I will be using the HTTPFoundation component, but for today I want to build a CLI script so no need for HTTPFoundation.
I  will be writing a small command line script to read and validate a  CSV file. For the  validation part I will be using the Symfony Validator Component,

Source of inspiration: https://blog.tinned-software.net/using-the-symfony-validator-as-a-standalone-component/


First step, install Symfony Validator using Composer:

                          composer require symfony/validator

and require the autoloader created by Composer.


<?php

require_once 'vendor/autoload.php';
require_once 'Loader.php';

use Symfony\Component\Validator\Validation;

$file = "someFile.csv";


$outputFile = "errorsLog" . "_" . time() . ".txt";
  
$validator = Validation::createValidatorBuilder()
 ->addMethodMapping('loadValidatorMetadata')
 ->getValidator()
;

$loader = new Loader($file, $outputFile, $validator);
$loader->load();

I will pass to my Loader class constructor the CSV file to be read, the file where I want to save the output and an instance of the Symfony validator.

The CSV file is read line by line, and from each line I will be creating a Row object. The validator will validate the Row object against the rules (constraints).
In the Row class I will add the method "loadValidatorMetada" mentioned when instantiating the Validator. I've added several validations for better examplification:


<?php

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;


class Row
{
    private $id;         
    private $name;   
    private $personalNumber;    
    private $CountryCode;  

    public function __construct($rowNumber, $data)
    {
        $this->id = $rowNumber;
        $this->name= $data[0];
        $this->personalNumber = $data[2];
        $this->countryCode = $data[1];
    }
 
 
    /**
     * This method is where you define your validation rules.
    */
    public static function loadValidatorMetadata(ClassMetadata $metadata)
    {
        //Name 
        $metadata->addPropertyConstraint('name', new Assert\NotBlank());
 
        //Personal number
        $metadata->addPropertyConstraint('personalNumber', new Assert\NotBlank());
        $metadata->addPropertyConstraint('personalNumber', new Assert\Type(array(
            'type'    => 'digit',
            'message' => 'The value {{ value }} is not a valid personal number.',
         )));
  
         //Country code
         $metadata->addPropertyConstraint('countryCode', new Assert\NotBlank());
         $metadata->addPropertyConstraint('countryCode', new Assert\Choice(array(
            'choices' => array('DE', 'AT', 'LU', 'ES', 'FR', 'BE', 'NO', 'SI', 'SE', 'IT', 'DK'),
            'message' => '{{ value }} is not a valid country code!',
          )));
  
     }
}


When validating a Row object against these constraints an array of Errors will be returned by the Validator. Below is the Loader class.



<?php

require_once 'Row.php';


class Loader
{
 private $fileName;
 private $validator;
 private $outputFile;
 
 public function __construct($fileName, $outputFile, $validator)
 {
  $this->fileName = $fileName;
  $this->outputFile = $outputFile;
  $this->validator = $validator;

 }
 
 public function load()
 {
  $file_handle = fopen($this->fileName, "r");
  $fileOutputHandle = fopen($this->outputFile, "w");
  
  /* 
   * Keep track of the current row in .csv file
   */
  $rowNumber=1;
  
  /* 
   * Read the file line by line
   */
  while (!feof($file_handle) ) {

   $line_of_text = fgetcsv($file_handle, 0);
   
   if ($rowNumber === 1) {
    
      /*
       * Ignore the first row from file as it contains headers
       */
     
   } else {
       $row = new Row($rowNumber, $line_of_text);    
       $this->validateRow($row, $fileOutputHandle); 
   }
   
   $rowNumber++;
  }
  fclose($file_handle);
  fclose($fileOutputHandle);
 }
 
 
 public function validateRow(Row $row, $fileOutputHandle)
 {
  $errors = $this->validator->validate($row);
  
  foreach ($errors as $error) {
   $errorMsg = "At row:" 
      . $error->getRoot()->getId() 
      . "- Property: " 
      . $error->getPropertyPath()
      . ' - Message: ' 
      . $error->getMessage()
      . "\n";
   
   fwrite($fileOutputHandle, $errorMsg);

  }
 }
}

Tuesday, July 12, 2016

Symfony/Sonata ACL - search for the object owner

Lately I am wrestling with the ACL in a project using Symfony with Sonata Admin Bundle and Sonata User Bundle. When editing objects from the admin panel offered by Sonata Admin Bundle things go smoothly, but the issues is how to set ACL permissions for objects I create in my own controllers.

Situation:

 An Admin1 user --> creates a Regular1 user --> which creates Objects and save them to database.

Problem:

Make Regular1 user and Admin1 user  owners of the newly created Object.

Note: Admin user is not ROLE_SUPER_ADMIN, just a custom role I have in my app

Solution:

First step, in the createAction controller method, after persisting my object, set the current user (Regular user) as owner of the object.


<?php 
    public function createAction(Request $request)
    {
        .....
        $em->persist($myObject);
        $em->flush();
            
        // retrieve services and get current user
        $adminSecurityHandler = $this->container->get('sonata.admin.security.handler');
        $modelAdmin = $this->container->get('admin.sites');
        $user = $this->getUser();

        $securityIdentity = UserSecurityIdentity::fromAccount($user);

        $objectIdentity = ObjectIdentity::fromDomainObject($myObject);
        $acl = $adminSecurityHandler->getObjectAcl($objectIdentity);
        
        if (is_null($acl)) {
            $acl = $adminSecurityHandler->createAcl($objectIdentity);
        }
        $adminSecurityHandler->addObjectClassAces($acl, $adminSecurityHandler->buildSecurityInformation($modelAdmin));
        $adminSecurityHandler->addObjectOwner($acl,$securityIdentity);  // set current user as owner in ACL

        $adminSecurityHandler->updateAcl($acl);

Second part is to search for the Admin user, owner of the regular user (which is currently logged).
Looking into the list of ACE associated to the object identity, I am searching for the one with  Mask equal to 128 (owner mask).


<?php

        //search for the owner (admin user) of the current user and give him privileges on the "myObject" object
        $userObjectIdentity = ObjectIdentity::fromDomainObject($user);
        $userObjectACL = $adminSecurityHandler->getObjectAcl($userObjectIdentity);

        $aces= $userObjectACL->getObjectAces();    
       
        /*
         *  $aces is an array containing ACEs, objects from this class: 
         *  http://api.symfony.com/2.7/Symfony/Component/Security/Acl/Domain/Entry.html
         */
        foreach($aces as $ace){
            if(128 === $ace->getMask()){
                $adminSecurityIdentity = $ace->getSecurityIdentity();
                $adminSecurityHandler->addObjectOwner($acl,$adminSecurityIdentity);  
                $adminSecurityHandler->updateAcl($acl);
            }
        }



Friday, July 1, 2016

Symfony DIC: setter injection

Most of the time, when defining a service I inject into constructor parameters or other services. But it is possible to use the Symfony DIC to inject parameters/services into setter methods:

     newsletter_manager:
         class:     NewsletterManager
         calls:
             - [setMailer, ['@my_mailer']]

http://symfony.com/doc/current/components/dependency_injection/types.html
 

Where:

setMailer  - is the name of the method
['@my_mailer'] - is the name of the service being injected

Practical example with Sonata

If you are using Sonata Admin Bundle and Sonata User Bundle, you may want to access the Entity Manager for some reasons. For this we will inject the service ''@doctrine.orm.entity_manager" using a setter method.

I will create a bundle named ApplicationSonataAdminUserBundle where I will place my new Admin class. In the project's services.yml I will add the new service definition:


sonata.user.admin.user:
        class: Application\Sonata\UserBundle\Admin\Model\UserAdmin
        arguments: [~,"%sonata.user.admin.user.entity%","SonataAdminBundle:CRUD"]
        calls:
            - [setUserManager, ['@fos_user.user_manager']]
            - [setTranslationDomain, ['%sonata.user.admin.user.translation_domain%']]
             - [setEntityManager, ["@doctrine.orm.entity_manager"]]
        tags:
            - { name: sonata.admin, manager_type: orm, group: "sonata_user", label: "users", label_catalogue: "SonataUserBundle",label_translator_strategy: "sonata.admin.label.strategy.underscore",icon: "<![CDATA[<i class='fa fa-users'></i>]]>" }


And of course in the class I will add the setter method "setAuthorizationChecker":

use Sonata\AdminBundle\Admin\AbstractAdmin;

class UserAdmin extends AbstractAdmin
{


    protected $entityManager;

    public function setEntityManager(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
    }


...
}