0
votes

We have a legacy app which is not based on symfony. Doctrine is in use and now we would like to add validation to the models. Seems that the Annotations never get autoloaded, even when "use" statements are in use.

[Semantical Error] The annotation "@Symfony\Component\Validator\Constraints\NotBlank" in property Test\Stackoverflow\User::$Username does not exist, or could not be auto-loaded.

Wrote a small demo application to showcase the problem and how we create the entity manager and validation instance.

composer.json:

{
    "require": {
        "symfony/validator"     :   "~3.1"
        , "doctrine/orm"        :   "~2.6.1"
    }
}

index.php

require_once ('vendor/autoload.php');

// Load Entities, would normally be done over composer since they reside in a package
require_once('test/User.php');
require_once('MyAnnotationTestApp.php');

// create test app
$app = new MyAnnotationsTestApp();
$app->initEntityManager('localhost', 'annotation_test', 'root', 'mysql', 3306);

if(key_exists('test', $_GET)){
    // Create entity and validate it
    $entity = new \Test\Stackoverflow\User();
    $entity->setUsername('StackoverflowUser');

    if($app->testAnnotationWithoutLoading($entity)){
        print "Seems the validation was working without preloading the asserts\n<br>";
    }

    if($app->testAnnotationWithLoading($entity)){
        print "Seems the validation was working because we loaded the required class ourself.\n<br>";
    }

    print "\n<br><br>The question is why the required annotation classes never get autoloaded?";

    }else{

    // Load the validator class otherwise the annotation throws an exception
    $notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();

    print "We have cerated the tables but also had to load the validator class ourself.\n<br>\n<br>";

    // create tables and
    $app->updateDatabaseSchema();
    print sprintf('<a href="%s?test">Now lets run the test</a>', $_SERVER['REQUEST_URI']);
} 

Doctrine user Entity

<?php
namespace Test\Stackoverflow;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity()
 * @ORM\Table(name="users")
 *
 */
class User{
    /**
     * @ORM\Id
     * @ORM\Column(name="Id",type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected $Id;

    public function getId(){
        return $this->Id;
    }

    /**
     * @ORM\Column(type="text", length=80, nullable=false)
     * @Assert\NotBlank()
     */
    protected $Username;

    /**
     * @return string
     */
    public function getUsername()
    {
        return $this->Username;
    }

    /**
     * @param string $Username
     */
    public function setUsername($Username)
    {
        $this->Username = $Username;
    }


} 

Demo App with doctrine/validator initialisation:

<?php


final class MyAnnotationsTestApp {

    /**
     * @var \Doctrine\ORM\EntityManager
     */
    private $entityManager;

    /**
     * @param string $host
     * @param string $database
     * @param string $username
     * @param string $password
     * @param integer $port
     * @param array $options
     * @return \Doctrine\ORM\EntityManager
     */
    public function initEntityManager($host, $database, $username, $password, $port, array $options=null){

        if($this->entityManager){
            return $this->entityManager;
        }

        $connectionString = sprintf('mysql://%3$s:%4$s@%1$s/%2$s', $host, $database, $username, $password, $port);
        $isDevMode = true;
        $dbParams = array(
            'url'               =>  $connectionString
            , 'driver'          =>  'pdo_mysql'
            , 'driverOptions'   =>   array(
                1002 =>     "SET NAMES utf8mb4"
            )
        );

        $cacheDriver = null;

        $config = \Doctrine\ORM\Tools\Setup::createAnnotationMetadataConfiguration(array(), $isDevMode, '.cache/', $cacheDriver, false);

        if($cacheDriver){
            $config->setMetadataCacheImpl($cacheDriver);
            $config->setQueryCacheImpl($cacheDriver);
            $config->setResultCacheImpl($cacheDriver);
        }


        $this->entityManager = \Doctrine\ORM\EntityManager::create($dbParams, $config);
        return $this->entityManager;
    }


    /**
     * @return \Doctrine\ORM\EntityManager
     */
    public function getEntityManager(){
        return $this->entityManager;
    }



    public function updateDatabaseSchema(){
        $metaData = array();
        $usedEntities = array(
            'Test\Stackoverflow\User'
        );
        foreach($usedEntities as $entity){
            $metaData[] = $this->entityManager->getClassMetadata($entity);
        }

        $tool = new \Doctrine\ORM\Tools\SchemaTool($this->entityManager);
        $tool->updateSchema($metaData);
        $this->generateProxies($metaData);
    }

    /**
     * Generate all the proxy classes for orm in the correct directory.
     * Proxy dir can be configured over application configuration
     *
     *
     * @throws \Exception
     */
    final public function generateProxies($metaData)
    {
        $em = $this->getEntityManager();
        $destPath = $em->getConfiguration()->getProxyDir();

        if (!is_dir($destPath)) {
            mkdir($destPath, 0777, true);
        }

        $destPath = realpath($destPath);

        if (!file_exists($destPath)) {
            throw new \Exception("Proxy destination directory could not be created " . $em->getConfiguration()->getProxyDir());
        }

        if (!is_writable($destPath)) {
            throw new \Exception(
                sprintf("Proxies destination directory '<info>%s</info>' does not have write permissions.", $destPath)
            );
        }

        if (count($metaData)) {
            // Generating Proxies
            $em->getProxyFactory()->generateProxyClasses($metaData, $destPath);
        }
    }


    /**
     * @var \Symfony\Component\Validator\Validator\ValidatorInterface
     */
    protected $validator;

    /**
     * @return \Symfony\Component\Validator\Validator\ValidatorInterface
     */
    final protected function getValidator(){
        if($this->validator){
            return $this->validator;
        }
        $this->validator = \Symfony\Component\Validator\Validation::createValidatorBuilder()
            ->enableAnnotationMapping()
            ->getValidator();
        return $this->validator;

    }


    /**
     * @param \Test\Stackoverflow\User $entity
     * @return bool
     */
    final public function testAnnotationWithoutLoading(\Test\Stackoverflow\User $entity){
        try {
            print "test to validate the entity without preloading the Assert classes\n<br>";
            $this->getValidator()->validate($entity);
            return true;
        } catch(\Exception $e){
            print "<strong>Does not work since the Asserts classes never get loaded: </strong> Exception-message: ".$e->getMessage()."\n<br>";
            return false;
        }
    }


    /**
     * @param \Test\Stackoverflow\User $entity
     * @return bool
     */
    final public function testAnnotationWithLoading(\Test\Stackoverflow\User $entity){


        // Here we force the autoloader to require the class
        $notBlankValidator = new \Symfony\Component\Validator\Constraints\NotBlank();

        try {
            print "Loaded the validator manually, will test of it fails now\n<br>";
            $this->getValidator()->validate($entity);
            return true;
        } catch(\Exception $e){
            print "<strong>Was not working: </strong> Exception-message: ".$e->getMessage()."\n<br>";
            print sprintf("<strong>Even when we autoload the class it is not working. Type of assert: %s</strong>\n<br>", get_class($notBlankValidator));
            return false;
        }
    }

} 
1
Found that adding the following lines was solving it but ::registerLoader is deprecated and will removed. What is the prefered way to handle this case? $loader = require_once ('vendor/autoload.php'); \Doctrine\Common\Annotations\AnnotationRegistry::registerLoader(array($loader, 'loadClass'));Rob
From the doctrine source: deprecated this method is deprecated and will be removed in doctrine/annotations 2.0. autoloading should be deferred to the globally registered autoloader by then. For now use AnnotationRegistry::registerLoader('class_exists')Rob
I believe that above is covered by Doctrine's own documentation?Jovan Perovic
@JovanPerovic Maybe you can send me a link because i found nothing. Appreciate it.Rob
Sorry, this is what I meant: doctrine-common.readthedocs.io/en/latest/reference/… -- The part "To anticipate the configuration section, making the above PHP class work with Doctrine Annotations requires this setup:"Jovan Perovic

1 Answers

0
votes

If you are using the Symfony Standard Edition, you must update your autoload.php file by adding the following code [1]

How are these annotations loaded? From looking at the code you could guess that the ORM Mapping, Assert Validation and the fully qualified annotation can just be loaded using the defined PHP autoloaders. This is not the case however: For error handling reasons every check for class existence inside the AnnotationReader sets the second parameter $autoload of class_exists($name, $autoload) to false. To work flawlessly the AnnotationReader requires silent autoloaders which many autoloaders are not. Silent autoloading is NOT part of the PSR-0 specification for autoloading. [2]

// at the top of the file
use Doctrine\Common\Annotations\AnnotationRegistry;

// at the end of the file
AnnotationRegistry::registerLoader(function($class) use ($loader) {
    $loader->loadClass($class);
    return class_exists($class, false);
});

[1] https://symfony.com/blog/symfony2-2-0-rc4-released

[2] https://www.doctrine-project.org/projects/doctrine-annotations/en/1.6/annotations.html