I'm doing some programming in Silex with the symfony components and I think I have found a bug with the symfony/serializer and the symfony/validator components.
First let me explain what I'm traing to achieve, then let's go to the code.
My objective is to annotate a class with information like serialization directives as well as validation directives. As the reading of these annotations can cost a litle cpu, I like to cache them in memory. For this purpose, I'm using memcache wrapper in the Doctrine/Common/Cache package.
The problem I face is that both the symfony/serializer and the symfony/validator write Metadata to the cache using the class name as key. When they try to retrieve the metadata later, they throw an exception, because the cache has invalid metadata, either an instance of Symfony\Component\Validator\Mapping\ClassMetadata or Symfony\Component\Serializer\Mapping\ClassMetadataInterface.
Following is a reproductible example (sorry if its big, I tried to make as small as possible):
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class Foo
{
/**
* #var int
* #Assert\NotBlank(message="This field cannot be empty")
*/
private $someProperty;
/**
* #return int
* #Groups({"some_group"})
*/
public function getSomeProperty() {
return $this->someProperty;
}
}
use Doctrine\Common\Annotations\AnnotationReader;
use \Memcache as MemcachePHP;
use Doctrine\Common\Cache\MemcacheCache as MemcacheWrapper;
$loader = require_once __DIR__ . '/../vendor/autoload.php';
\Doctrine\Common\Annotations\AnnotationRegistry::registerLoader([$loader, 'loadClass']);
$memcache = new MemcachePHP();
if (! $memcache->connect('localhost', '11211')) {
throw new \Exception('Unable to connect to memcache server');
}
$cacheDriver = new MemcacheWrapper();
$cacheDriver->setMemcache($memcache);
$app = new \Silex\Application();
$app->register(new Silex\Provider\SerializerServiceProvider());
$app['serializer.normalizers'] = function () use ($app, $cacheDriver) {
$classMetadataFactory = new Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory(
new Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader(new AnnotationReader()), $cacheDriver);
return [new Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer($classMetadataFactory) ];
};
$app->register(new Silex\Provider\ValidatorServiceProvider(), [
'validator.mapping.class_metadata_factory' =>
new \Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory(
new \Symfony\Component\Validator\Mapping\Loader\AnnotationLoader(new AnnotationReader()),
new \Symfony\Component\Validator\Mapping\Cache\DoctrineCache($cacheDriver)
)
]);
$app->get('/', function(\Silex\Application $app) {
$foo = new Foo();
$app['validator']->validate($foo);
$json = $app['serializer']->serialize($foo, 'json');
return new \Symfony\Component\HttpFoundation\JsonResponse($json, \Symfony\Component\HttpFoundation\Response::HTTP_OK, [], true);
});
$app->error(function (\Exception $e, \Symfony\Component\HttpFoundation\Request $request, $code) {
return new \Symfony\Component\HttpFoundation\Response('We are sorry, but something went terribly wrong.' . $e->getMessage());
});
$app->run();
After running this example you get fatal errors.
Can anyone confirm that I'm not making a hard mistake here?
Currently my workaround for this is rewrite the DoctrineCache class making use of a namespace for the cache keys. Its working, but I think its ugly.
I think what you need to do is two separate CacheDrivers. See https://github.com/doctrine/cache/blob/master/lib/Doctrine/Common/Cache/CacheProvider.php for how namespaces are used there.
You could:
$validatorCacheDriver = new MemcacheWrapper();
$validatorCacheDriver->setMemcache($memcache);
$validatorCacheDriver->setNamespace('symfony_validator');
$serializerCacheDriver = new MemcacheWrapper();
$serializerCacheDriver->setMemcache($memcache);
$serializerCacheDriver->setNamespace('symfony_serializer');
// note that the two drivers are using the same memcache instance,
// so only one connection will be used.
$app['serializer.normalizers'] = function () use ($app, $serializerCacheDriver) {
$classMetadataFactory = new Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory(
new Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader(new AnnotationReader()), $serializerCacheDriver);
return [new Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer($classMetadataFactory) ];
};
$app->register(new Silex\Provider\ValidatorServiceProvider(), [
'validator.mapping.class_metadata_factory' =>
new \Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory(
new \Symfony\Component\Validator\Mapping\Loader\AnnotationLoader(new AnnotationReader()),
new \Symfony\Component\Validator\Mapping\Cache\DoctrineCache($validatorCacheDriver)
)
]);
I've trimmed the code to only show the parts that play some part in the solution. I hope this helps!
Related
We use the Symfony Serializer to convert some Doctrine objects to JSON, goal is to provide them as results of API calls.
Out data model has about thirty classes, all are somehow linked with each other and the Doctrine model reflects this. So navigation from one instance to other linked instances is easy.
Now, we are pretty happy that no changes are necessary when we add a new attribute to a class. However, the situation is different is a new link is added, this often adds way too much information because the linked classes also have links and they have links and arrays of objects...
To restrict this, we typically add ignored attributes:
protected function serialize($e, $ignored = ['user'])
{
if ($this->container->has('serializer')) {
$this->serializer = $this->container->get('serializer');
} else {
throw new ServiceNotFoundException('Serializer not found');
}
return $this->serializer->serialize($e, 'json', [
'ignored_attributes' => $ignored,
ObjectNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object) {
if (method_exists($object, 'getUuid')) {
return $object->getUuid();
} elseif (method_exists($object, 'getId')) {
return $object->getId();
} elseif (method_exists($object, '__toString')) {
return $object->__toString();
} else {
return '[Reference to object of type ' . get_class($object) . ']';
}
},
]);
}
And then:
return new Response($this->serialize(
[
'presentation' => $presentation,
],
[
'zoomUser',
'userExcelDownloads',
'presentationUserTopics',
'addedBy',
'user',
'presentations',
'sponsorScopes',
'sponsorPresentations',
'showroomScope',
'presentationsForTopic',
'presentationsForTopics',
'presentationHistories',
'showroomTopics',
'presentation',
'presentationHistory',
]
));
This works but maintenance is horrible - when ever the database model is changed, there is the risk that an API call emits a few more MB because a new attribute would have to be ignored. There is no way of finding this.
So how do you handle this?
One solution could be to restrict the serialization depth similar to the CIRCULAR_REFERENCE_HANDLER, i.e., for objects on level three just add the IDs and not the full objects. How could I build this?
Symfony serializer has built-in Ignore strategy (https://symfony.com/doc/current/components/serializer.html#ignoring-attributes)
you can ignore the attribute directly from the model.
use Symfony\Component\Serializer\Annotation\Ignore;
class Presentation
{
/**
* #Ignore()
*/
public $zoomUser;
//...
}
or by using context.
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
$normalizer = new ObjectNormalizer();
$encoder = new JsonEncoder();
$serializer = new Serializer([$normalizer], [$encoder]);
$serializer->serialize($presentation, 'json', [AbstractNormalizer::IGNORED_ATTRIBUTES => ['zoomUser']]);
We switched to JMS Serializer Bundle where setting the max. depth is very simple and helps us a lot.
https://jmsyst.com/bundles/JMSSerializerBundle
For Symfony serializer, the only way is to use serialization groups.
I started using zend 3 a few months ago for a project and now I'm stuck.
I have a customized authentication (not using zend authentication module) which is working fine but I need to validate every time i access a redirected page.
Because on every page's url goes a token that is used to check in database, and I'm trying to do inside the function onBootStrap().
I learned to use factories, models, mappers and I'm currently using them in some controllers, but i can't find a way to achieve, at least if i could get the dbAdapter from the bootstrap event to use, it will be enough.
Any thoughts?
You can use lazy event for this job. here's you are a simple example.
public function onBootstrap(EventInterface $e)
{
/** #var \Interop\Container\ContainerInterface $container */
$container = $e->getApplication()->getserviceManager();
$events = $e->getApplication()->getEventManager();
$events->attach(MvcEvent::EVENT_ROUTE, new LazyListener([
'listener' => Listener::class,
'method' => 'onRoute'
], $container));
}
So you can check you auth on Listener class' "onRoute" method. If MvcEvent::ROUTE event is too early for you, you can use other MvcEvents too.
Hope this can solve your problem.
To get the adapter and use it in tablegateway
public function onBootstrap(EventInterface $event){
$container = $event->getApplication()->getServiceManager();
$dbAdapter = $container->get(AdapterInterface::class);
$resultSetPrototype = new ResultSet();
$resultSetPrototype->setArrayObjectPrototype(new TableObjectClass());
$tableGateway = new TableGateway("Table name", $dbAdapter, null, $resultSetPrototype);
$mapper = new TableObjectClassMapper($tableGateway);
//and use $mapper to get data from the table and store it as TableObjectClass
$data = $mapper->fetch()->current();
}
So I'm trying to configure the FOSHttpCacheBundle (latest release ^2.0) with the default Symfony HttpCache.
Here is the content of my AppCache.php according to the docs:
<?php
use FOS\HttpCache\SymfonyCache\CacheInvalidation;
use FOS\HttpCache\SymfonyCache\EventDispatchingHttpCache;
use FOS\HttpCache\SymfonyCache\DebugListener;
use FOS\HttpCache\SymfonyCache\CustomTtlListener;
use FOS\HttpCache\SymfonyCache\PurgeListener;
use FOS\HttpCache\SymfonyCache\RefreshListener;
use FOS\HttpCache\SymfonyCache\UserContextListener;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\HttpCache\StoreInterface;
use Symfony\Component\HttpKernel\HttpCache\SurrogateInterface;
class AppCache extends HttpCache implements CacheInvalidation
{
use EventDispatchingHttpCache;
/**
* Overwrite constructor to register event listeners for FOSHttpCache.
*/
public function __construct(
HttpKernelInterface $kernel,
StoreInterface $store,
SurrogateInterface $surrogate = null,
array $options = []
) {
parent::__construct($kernel, $store, $surrogate, $options);
$this->addSubscriber(new CustomTtlListener());
$this->addSubscriber(new PurgeListener());
$this->addSubscriber(new RefreshListener());
$this->addSubscriber(new UserContextListener());
if (isset($options['debug']) && $options['debug']) {
$this->addSubscriber(new DebugListener());
}
}
/**
* Made public to allow event listeners to do refresh operations.
*
* {#inheritDoc}
*/
public function fetch(Request $request, $catch = false)
{
return parent::fetch($request, $catch);
}
}
Now according to the Symfony docs, enabling the caching proxy is just a matter of setting the app.php file as follow (uncommenting that last line):
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();
$kernel = new AppCache($kernel);
Which gives me a nice:
PHP message: PHP Catchable fatal error: Argument 2 passed to
Symfony\Component\HttpKernel\HttpCache\HttpCache::__construct() must
be an instance of
Symfony\Component\HttpKernel\HttpCache\StoreInterface, none given,
called in /php/api/current/web/app.php on line 11 and defined in
/php/api/current/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
on line 78
Which makes perfect sense given the HttpCache class constructor.
So the question is, is it the doc that's not up to date, or is it just me missing something really obvious?
use Symfony\Component\HttpKernel\HttpCache\Store;
$kernel = new AppKernel('dev', true);
$kernel->loadClassCache();
$store = new Store('app/cache');
$kernel = new AppCache($kernel, $store);
I am new to ZF2 and I want to test the login method in a legacy application. Or introduce Unit tests in old code :).
The code that I have is not done according to the manual; it seems super strange if I compare it to the manual examples or even best practices.
I the login method like this:
http://pastebin.com/ZzvuBcGe
in this case the legacy is that Helper, Carts, Users, Userslogs and Usertests are models .... all of them extend DB.
In the module.config.php I have this code:
'service_manager' => array(
'factories' => array(
'Zend\Db\Adapter\Adapter' => 'Zend\Db\Adapter\AdapterServiceFactory',
'AuthService' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$dbTableAuthAdapter = new DbTableAuthAdapter(
$dbAdapter,
'tbl_user',
'USER_LOGIN',
'USER_PASSWORD',
'MD5(?)'
);
$authService = new AuthenticationService();
$authService->setAdapter($dbTableAuthAdapter);
$authService->setStorage(new StorageSession('session'));
return $authService;
},
'Helper' => function ($sm) {
return new Helper($sm);
},
'Users' => function ($sm) {
return new Users($sm);
},
'Carts' => function ($sm) {
return new Carts($sm);
}
...
I know that the DbTableAuthAdapter is deprecated but I have to understand how to modify this in order to change it in the best way possible. I have the feeling if I change this all the User, Carts etc models will crash.
My Unit test is like this for the moment:
<?php namespace ApplicationTest\Controller;
use Application\Controller\LoginController;
use Zend\Stdlib\ArrayUtils;
use Zend\Test\PHPUnit\Controller\AbstractHttpControllerTestCase;
class LoginControllerTest extends AbstractHttpControllerTestCase
{
protected $traceError = true;
public function setUp()
{
parent::setUp();
// The module configuration should still be applicable for tests.
// You can override configuration here with test case specific values,
// such as sample view templates, path stacks, module_listener_options,
// etc.
$configOverrides = [];
$this->setApplicationConfig(ArrayUtils::merge(
// Grabbing the full application configuration:
include __DIR__ . '/../../../../../config/application.config.php',
$configOverrides
));
}
public function loginCredentialsProvider()
{
return [
['userDev', '12345'],
];
}
/**
* #covers LoginController::loginAction()
* #dataProvider loginCredentialsProvider
* #param $username
* #param $password
*/
public function testLogin($username, $password)
{
// prepare request
//$this->getRequest()
//->setMethod('POST')
//->setPost(new Parameters(array(
//'user_login' => $username,
//'user_password' => $password
//)));
$helperMock = $this->getMockBuilder('Application\Model\Helper')
->disableOriginalConstructor()
->getMock();
$serviceManager = $this->getApplicationServiceLocator();
$serviceManager->setAllowOverride(true);
$serviceManager->setService('Application\Model\Helper', $helperMock);
// send request
$this->dispatch('/login', 'POST', $this->loginCredentialsProvider());
$this->assertEquals('userDev12345', $username . $password);
// $this->markTestIncomplete('login incomplete');
}
/**
* #depends testLogin
*/
public function testLogout()
{
$this->markTestIncomplete('logout incomplete');
}
}
I tried different ways to test but no succes and of course that I get errors:
Zend\ServiceManager\Exception\ServiceNotCreatedException: An exception was raised while creating "Helper"; no instance returned
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:930
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
Caused by
Zend\ServiceManager\Exception\ServiceNotCreatedException: An exception was raised while creating "Zend\Db\Adapter\Adapter"; no instance returned
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:930
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1055
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Model/DB.php:17
/project/module/Application/config/module.config.php:1324
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
Caused by
PHPUnit_Framework_Error_Notice: Undefined index: db
/project/vendor/zendframework/zendframework/library/Zend/Db/Adapter/AdapterServiceFactory.php:26
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1055
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Model/DB.php:17
/project/module/Application/config/module.config.php:1324
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:923
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:1057
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:633
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:593
/project/vendor/zendframework/zendframework/library/Zend/ServiceManager/ServiceManager.php:525
/project/module/Application/src/Application/Controller/LoginController.php:38
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractActionController.php:83
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Controller/AbstractController.php:116
/project/vendor/zendframework/zendframework/library/Zend/Mvc/DispatchListener.php:113
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:468
/project/vendor/zendframework/zendframework/library/Zend/EventManager/EventManager.php:207
/project/vendor/zendframework/zendframework/library/Zend/Mvc/Application.php:313
/project/vendor/zendframework/zendframework/library/Zend/Test/PHPUnit/Controller/AbstractControllerTestCase.php:282
/project/module/Application/test/ApplicationTest/Controller/LoginControllerTest.php:69
/project/vendor/phpunit/phpunit/phpunit:47
The issues that I have are first how to get the test to pass with this code? I know that normally you do the test and after that the code but I need a starting point to understand the mess that I have in the application. Second, what is the easy or the best way to modify the "models" to not be a dependency for each method and then pass the test? How to modify the deprecated DbTableAuthAdapter in order not to brake all things?
Like i said I am new to ZF2 and Phpunit and I am stuck over this messy code and I have the best practices in my mind but I don't know how to put them in action in this code. Thank you for all the info that I will receive for this.
LATER EDIT
the solution is to add this line in the test, foreach model:
// access via application object..
$bla = $this->getApplication()->getServiceManager()->get('Tests');
the solution is to add this line in the test, foreach model:
$bla = $this->getApplication()->getServiceManager()->get('Tests');
Thank you i336_ :)
I'm writing my own PHP framework built on top of Symfony components as a learning exercise. I followed the tutorial found at http://symfony.com/doc/current/create_framework/index.html to create my framework.
I'd now like to wire up my routes against my controllers using annotations. I currently have the following code to setup the routing:
// Create the route collection
$routes = new RouteCollection();
$routes->add('home', new Route('/{slug}', [
'slug' => '',
'_controller' => 'Controllers\HomeController::index',
]));
// Create a context using the current request
$context = new RequestContext();
$context->fromRequest($request);
// Create the url matcher
$matcher = new UrlMatcher($routes, $context);
// Try to get a matching route for the request
$request->attributes->add($matcher->match($request->getPathInfo()));
I have come across the following class to load the annotations but I'm not sure how to use it:
https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Routing/Loader/AnnotationDirectoryLoader.php
I'd appreciate it if someone could help.
Thanks
I've finally managed to get this working. First I changed where I included the autoload.php file to the following:
use Doctrine\Common\Annotations\AnnotationRegistry;
$loader = require __DIR__ . '/../vendor/autoload.php';
AnnotationRegistry::registerLoader([$loader, 'loadClass']);
Then I changed the routes collection bit (in the question) to:
$reader = new AnnotationReader();
$locator = new FileLocator();
$annotationLoader = new AnnotatedRouteControllerLoader($reader);
$loader = new AnnotationDirectoryLoader($locator, $annotationLoader);
$routes = $loader->load(__DIR__ . '/../Controllers'); // Path to the app's controllers
Here's the code for the AnnotatedRouteControllerLoader:
class AnnotatedRouteControllerLoader extends AnnotationClassLoader {
protected function configureRoute(Route $route, ReflectionClass $class, ReflectionMethod $method, $annot) {
$route->setDefault('_controller', $class->getName() . '::' . $method->getName());
}
}
This has been taken from https://github.com/sensiolabs/SensioFrameworkExtraBundle/blob/master/Routing/AnnotatedRouteControllerLoader.php. You may wish to modify it to support additional annotations.
I hope this helps.