Symfony 5 Serializer - How to not expose whole database - php

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.

Related

Where is ->getForm() method located for Symfony3 CreateFormFactory

First off, I am building using Symfony components. I am using 3.4. I was following the form tutorial https://symfony.com/doc/3.4/components/form.html which lead me to this page
https://symfony.com/doc/current/forms.html#usage
I noticed that Symfony added a Form directory to my application.
This was great! I thought I was on my way. So, in my controller, I added this line.
$form = Forms::createFormFactory();
When I tried loading the page, everything went well with no error messages until I added the next two lines.
->addExtension(new HttpFoundationExtension())
->getForm();
I removed the ->addExtension(new HttpFoundationExtension()) line and left the ->getForm() thinking it would process without the add method call. It did not. So, I backed up to see if the IDE would type hint for me.
In the IDE PHPStorm, these are the methods that I have access to but not getForm per the tutorial
Every tutorial I have tried ends with not being able to find some method that does not exist. What do I need to install in order to have access to the ->getForm() method?
UPDATE:
I have made a couple of steps forward.
$form = Forms::createFormFactory()
->createBuilder(TaskType::class);
The code above loads with no errors. (Why is still fuzzy). But next stop is the createView(). None existant also. I only get hinted with create().
Reading between the lines in this video help with the last two steps. https://symfonycasts.com/screencast/symfony3-forms/render-form-bootstrap#play
UPDATE 2:
This is what I have now.
$session = new Session();
$csrfManager = new CsrfTokenManager();
$help = new \Twig_ExtensionInterface();
$formFactory = Forms::createFormFactoryBuilder()
->getFormFactory();
$form = $formFactory->createBuilder(TaskType::class)
->getForm();
//$form->handleRequest();
$loader = new FilesystemLoader('../../templates/billing');
$twig = new Environment($loader, [
'debug' => true,
]);
$twig->addExtension(new HeaderExtension());
$twig->addExtension(new DebugExtension());
$twig->addExtension($help, FormRendererEngineInterface::class);
return $twig->render('requeueCharge.html.twig', [
'payments' => 'Charge',
'reportForm' => $form->createView()
]);
Does anyone know of an update standalone for example? The one that everyone keeps pointing two is 6 years old. There have been many things deprecated in that time period. So, it is not an example to follow.
Your Form class and method createFormFactory must return object that implement FormBuilderInterface then getForm method will be available. You need create formBuilder object.
But this can't be called from static method because formBuilder object need dependency from DI container. Look at controller.
If you want you need register your own class in DI and create formBuilder object with dependencies and return that instance of object.
EDIT
You don't need to use abstract controller. You can create your own class which is registered in DI for geting dependencies. In that class you create method which create new FormBuilder(..dependencies from DI from your class ...) and return instance of that FormBuilder. Then you can inject your class in controller via DI.
Example (not tested)
// class registered in DI
class CustomFormFactory
{
private $_factory;
private $_dispatcher;
public CustomFormFactory(EventDispatcherInterface $dispatcher, FormFactoryInterface $factory)
{
$_dispatcher = $dispatcher;
$_factory = $factory;
}
public function createForm(?string $name, ?string $dataClass, array $options = []): FormBuilderInterface
{
// create instance in combination with DI dependencies (factory..) and your parameters
return new FormBuilder($name, $dataClass, $_dispatcher, $_factory, $options);
}
}
Usage
$factory = $this->container->get('CustomFormFactory');
$fb = $factory->createForm();
$form = $fb->getForm();

Is this data being overwritten by another component?

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!

Transform entity for smaller output

I am trying to create a simple calendar using Symfony. For this I want to request the events of the selected month via AJAX. Each event can have multiple users connected to it.
Now retrieving the event is quite simple
$events = $this->em->getRepository('AppBundle:Event')
->findByYearMonth($this->getUser(), $year, $month);
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
$serializer = new Serializer([$normalizer], [$encoder]);
$serialized = $serializer->serialize($events, 'json');
$response = new Response($serialized);
$response->headers->set('Content-Type', 'application/json');
I needed to setCircularReferenceHandler because obviously users have the events connected to them as well.
In my test I currently have only 2 events, yet I get a response of ~50kb, simply because it gets bloated by the user objects. All I really want is user ID and name, but it retrieves all other fields (and other connected entities).
The circular reference handler itself only gets active once the user is already returned (and catches the event within the user that originally retrieved the user).
I have already searched about this but did not find a single result that went in the way I wanted to (most were about forms, general serialization, etc).
The only idea I had was adding a method to the Event entity, eg getSimple where I manually set some parameters and leave outers out (but have to do so for everything connected as well). This seemed quite unclean, hence I am asking here.
TL;DR
I want something like
[
{"id":1, "name":"foo", "users":[
{"id":1, "name":hans"},
{"id":2, "name":"jack"},...
]},...
]
but I get something like
[
{"id":1, "name":"foo", "users":[
{"id":1, "name":hans", "events":[1,{"id":3, "name":"this hasnt already been shown"}], "userfield":"var", "anotheruserfield":"bar", "someOtherRelation":{"bla"},
{"id":2, "name":"jack", "events":[1,{"id":3, "name":"this hasnt already been shown"}], "userfield":"var", "anotheruserfield":"bar", "someOtherRelation":{"bla"}},...
]},...
]
If I correctly understand, you want to exclude the events property (and maybe others) from your objects serialization.
The JMSSerializer would fits your needs as it allows to exclude/expose properties and/or limit their depth.
The bundle is mentioned at the end of the Serialization chapter of the Symfony documentation.
See exclusion strategies and its #Exclude, #Expose, #Groups and #MaxDepth annotations.
Yes, JMSSerializer provides Exclude and other methods which might solve this, but I decided for something different: Writing a custom EventNormalizer which does exactly what I want and doesn't change the other entities per se.
$encoder = new JsonEncoder();
$normalizer = new \AppBundle\Serializer\Normalizer\EventNormalizer();
$serializer = new Serializer([$normalizer], [$encoder]);
$serialized = $serializer->serialize($events, 'json');
And the normalizer
namespace AppBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Event normalizer
*/
class EventNormalizer implements NormalizerInterface
{
/**
* {#inheritdoc}
*/
public function normalize($object, $format = null, array $context = array())
{
$result = /* using fields from $object */
return $result;
}
/**
* {#inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \AppBundle\Entity\Event;
}
}

Consistent REST API Response in Laravel+Dingo

I have been developing a set of rest APIs to be exposed for mobile apps. I am following the repository pattern for the development on the Laravel project. How do I implement a presenter and transformer for formatting a constant JSON output throughout the set of all my APIs?
For example I have the following controller for login
public function authenticate()
{
$request = Request::all();
try {
// If authenticated, issue JWT token
//Showing a dummy response
return $token;
} catch (ValidatorException $e) {
return Response::json([
'error' =>true,
'message' =>$e->getMessageBag()
]);
}
}
Now where does a transformer and presenter come into the picture? I know that both are used to format the output by converting the db object and produce a formatted JSON so that it remains uniform across my APIs.
The dingo API and fractal or even the framework (L5 repository) don't provide detailed documentation and I can't find any tutorials on this.
I have created the following presenter and transformer for another API which gives the list of products
namespace App\Api\V1\Transformers;
use App\Entities\Product;
use League\Fractal\TransformerAbstract;
class UserTransformer extends TransformerAbstract {
public function transform(\Product $product)
{
return [
'id' => (int) $product->products_id
];
}
}
Presenter
<?php
namespace App\Api\V1\Presenters;
use App\Api\V1\Transformers\ProductTransformer;
use Prettus\Repository\Presenter\FractalPresenter;
/**
* Class ProductPresenter
*
* #package namespace App\Presenters;
*/
class ProductPresenter extends FractalPresenter
{
/**
* Transformer
*
* #return \League\Fractal\TransformerAbstract
*/
public function getTransformer()
{
return new UserTransformer();
}
}
How will I set the presenter in the controller and respond back? Tried
$this->repository->setPresenter("App\\Presenter\\PostPresenter");
But it doesn't seems to work and the doc doesn't shows the complete steps.
In the above example, how can I make a template for an error response which I can use throughout my APIs and how will I pass my error exceptions to it?
It seems like presenter and transformer can be used to convert database objects into presentable JSON and not anything else. Is that right?
How do you use a presenter and a transformer for a success response and an error response? By passing exceptions, instead of DB objects to the transformer?
I had the same exact problem and here is how I used dingo with transformer
Controller:
public function update(Request $request)
{
$bus = new CommandBus([
$this->commandHandlerMiddleware
]);
$agency = $bus->handle(
new UpdateAgencyCommand($request->user()->getId(), $request->route('id'), $request->only('name'))
);
return $this->response->item($agency, new AgencyTransformer());
}
Transformer:
class AgencyTransformer extends TransformerAbstract
{
public function transform(AgencyEntity $agencyEntity)
{
return [
'id' => (int) $agencyEntity->getId(),
'name' => $agencyEntity->getName(),
];
}
}
and this is how I handle errors:
throw new UpdateResourceFailedException('Could not update agency.', $this->agencyUpdateValidator->errors());
I just now see your similar question here as well. So see my answer on your other question here: https://stackoverflow.com/a/34430595/429719.
From the other question I derived you're using Dingo, so use that as a structured response class. Make sure you're controller extends from Dingo and then you can just return items and collections in a structured way like:
return $this->response->item($user, new UserTransformer);
return $this->response->collection($users, new UserTransformer);
If you want a nice error handling look for the docs here: https://github.com/dingo/api/wiki/Errors-And-Error-Responses
Basically you can throw any of the core exceptions or a few custom Dingo ones. The Dingo layer will catch them and returns a structured JSON response.
As per the Dingo docs:
throw new Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException('Nope, no entry today!');
Will generate:
{
"message": "Nope, no entry today!",
"status_code": 403
}
You can try use,[Midresapi]: https://github.com/oktorino/midresapi. this will return consistens success or failled response, work in laravel 7 & 8 , Handling validation response, handling 500 response:
$users=\App\User::latest()->limit(2)->get();
return response($users);
#or
return fractal()
->collection($users)
->transformWith(new \App\Transformers\UserTransformer)
->toArray();
Response :
{
"status_code": 200,
"success": true,
"message": "ok",
"data": [
{
"username": "dany",
"email": "jancuk#sabarbanget.com"
},
{
"username": "scc-client-5150",
"email": "dancuk#gmail.com"
}
]
}
Fractal is fully documented here: http://fractal.thephpleague.com/
There is an excellent book that I regularly read from the Phil Sturgeon https://leanpub.com/build-apis-you-wont-hate You can find most of the books code available in github https://github.com/philsturgeon/build-apis-you-wont-hate. You can find really nice examples of Fractal in there.
I would create an Api Controller and extend it from my Controllers.In there there should be all the respond functions (respondWithError, respondWithArray etc.)
Tranformers are transforming objects in a consistent json format so that all your endpoints return the same thing for each entity.
Dont really have an answer on this
There are enough examples in Fractal documentation.

Zend framework 2: AbstractListenerAggregate

I have a question about Zendframework 2, Event manager and listener.
class ApiErrorListener extends AbstractListenerAggregate {
public function attach(EventManagerInterface $events)
{
$this->listeners[] = $events->attach(MvcEvent::EVENT_RENDER, __CLASS__ . '::onRender', 1000);
}
public static function onRender(MvcEvent $e)
{
if($e->getResponse()->isOk())
{
return;
}
$httpCode = $e->getResponse()->getStatusCode();
$sm = $e->getApplication()->getServiceManager();
$viewModel = $e->getResult();
$exception = $viewModel->getVariable('exception');
$model = new JsonModel(
array(
'errorCode' => !empty($exception) ? $exception->getCode() : $httpCode,
'errorMsg' => !empty($exception) ? $exception->getMessage() : NULL
)
);
$model->setTerminal(true);
$e->setResult($model);
$e->setViewModel($model);
$e->getResponse()->setStatusCode($httpCode);
}
}
I think ApiErrorListener should be a listener, or say it is an observer. Why it has to implement attach() function?
Observer Design Pattern
In this link, you can see, only Subject (broadcaster attach or detach listeners).
I think I get confused ...
Anyone please help.
Thanks in advance.
The AbstractListenerAggregate provides functionality to attach/detachs a group of events with similar scope and purpose.
It's more of an organizational class than functionality. It provides an easy way to attach and detach events. In the case of event detaching, it provides an easy way to detach a group of events without any complicated logic to loop over ALL registered events and find the ones you're looking for.
You don't have to use the listener aggregate, you can certainly attach events to the EventManager anywhere you want in an application. However, as your application grows and there are more and more events with more and more dependencies, it can provide some sanity (AND TESTABILITY) to your event logic.

Categories