Having PHP, Symfony 4.4, JMS Serializer and json payload (request body) as such:
{
"quantity": 1,
"product": {
"sku": "bla"
},
"myId": {
"id": "0010N00005GcOhhQAF"
}
}
This payload is send to my endpoint and everything is correctly deserialized into correct resulting CustomRequest object - also nested objects like Product and MyId is correctly created. In the case of the Product object it is ok, because Product have complex structure with multiple attributes.
But what I would like to achieve is to make the input of myId easier. I would like to have it instead of:
"myId": {
"id": "0010N00005GcOhhQAF"
}
having simple this:
"myId": "0010N00005GcOhhQAF"
You may be asking why do I have class for simple id. It is not simple id, it have some special validation and bussiness logic inside and is used through out all application, so for validation purposes its better to have an object for it.
So lets say, I want my deserializer to automatically take that simple id string into the constructor of the class MyId, and return the object. MyId class is simple class like:
class MyId
{
/**
* #AppAssert\MyId()
*/
private ?string $value = null;
public function __construct(string $value)
{
$this->value = $value;
}
public function __toString()
{
return $this->value;
}
}
FYI: I tried this annotation in the resulting CustomRequest object, but it was not working
/**
* #Serializer\Type("App\Model\MyId")
*/
private ?MyId $myId = null;
EDIT: Another important part: this is how the endpoint automatically transforms request body into the CustomRequest object. Here you can see, that I am using ParamConverter for it.
/**
* #Rest\Post("/product")
* #ParamConverter("customRequest", options={
* "validator"={"groups"={"Default","product"="create"}},
* })
*/
public function postCreateProductAction(CustomRequest $customRequest) {
// ...
}
The question is: How to use JMS Serializer with Symfony to make it work. To take simple one string pass it atomatically to constructor and make and object from it. Is this even possible? Thanks
You need to write custom (De)Normalizer.
What is it: https://symfony.com/doc/current/components/serializer.html#normalizers
How to customize: https://symfony.com/doc/current/serializer/custom_normalizer.html
I made it working by using JMS\Serializer\Handler\SubscribingHandlerInterface. With this approach you can just simply add callbacks that kicks in during serialization/deserialization process. See code below that shows exact solution for MyId object.
More info about this technique here: https://jmsyst.com/libs/serializer/master/handlers
namespace App\Handlers;
use App\Model\MyId;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigatorInterface;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonDeserializationVisitor;
use JMS\Serializer\JsonSerializationVisitor;
class MyIdHandler implements SubscribingHandlerInterface
{
/**
* #return array<int, array<string, int|string>>
*/
public static function getSubscribingMethods(): array
{
return [
[
'direction' => GraphNavigatorInterface::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => MyId::class,
'method' => 'serializeMyIdToJson',
],
[
'direction' => GraphNavigatorInterface::DIRECTION_DESERIALIZATION,
'format' => 'json',
'type' => MyId::class,
'method' => 'deserializeMyIdFromJson',
],
];
}
/**
* #param array<mixed> $type
*/
public function serializeMyIdToJson(JsonSerializationVisitor $visitor, ?MyId $myId, array $type, Context $context): string
{
if ($myId === null) {
return '';
}
return (string)$myId;
}
/**
* #param array<mixed> $type
*/
public function deserializeMyIdFromJson(JsonDeserializationVisitor $visitor, string $myId, array $type, Context $context): ?MyId
{
if (empty($myId)) {
return null;
}
return new MyId($myId);
}
}
Related
How can I choose(filter) on my controller which fields I want (or don't want) to pass to my frontend?
my Controller:
/**
* #Route("/", name="dashboard")
*/
public function index()
{
$aniversariantes = $this->getDoctrine()->getRepository(Usuario::class)->aniversariantes();
return $this->render('dashboard/index.html.twig', [
'controller_name' => 'DashboardController',
'aniversariantes' => $aniversariantes
]);
}
My repository:
/**
* #return []
*/
public function aniversariantes(): array
{
$qb = $this->createQueryBuilder('u')
->andWhere('u.ativo = 1')
->andwhere('extract(month from u.dtNascimento) = :hoje')
->setParameter('hoje', date('m'))
->getQuery();
return $qb->execute();
}
Dump from entity:
What can I do if I don't want to pass the "password" field for example?
If you are just trying to prevent certain fields from being dumped, it is useful to know
Internally, Twig uses the PHP var_dump function.
https://twig.symfony.com/doc/2.x/functions/dump.html
This means you can can define the PHP magic method __debugInfo in your entity
This method is called by var_dump() when dumping an object to get the properties that should be shown. If the method isn't defined on an object, then all public, protected and private properties will be shown.
https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo
So in your entity do something like this:
class Usuario {
...
public function __debugInfo() {
return [
// add index for every field you want to be dumped
// assign/manipulate values the way you want it dumped
'id' => $this->id,
'nome' => $this->nome,
'dtCadastro' => $this->dtCadastro->format('Y-m-d H:i:s'),
];
}
...
}
Currently I'm stuck in making an accessor. I'm trying to access some values from the nested relationship after when I got that I'm returning the value and appending it to the model, but the problem is inside my response I'm getting values of the relationship which I try to access in my accessor.
public function getTranslatorEmailAttribute()
{
if (in_array(AddOnConfirmation::EMAIL, $this->customer->department->company->add_on_confirmation)) {
return $this->assignedTranslator()->first()->email;
} else {
return null;
}
}
Here is the customer relation which I'm trying to use
public function customer()
{
return $this->belongsTo(User::class)->with('customerData.customerType', 'customerData.department.company');
}
How can I fix this?
Here is a screenshot of response I'm getting with using accessor
Your GET route that handles api/bookings/{id} should return the resource in the end, something like:
return BookingResource::make($booking);
Then create BookingResource and likely put it in namespace App\Http\Resources\Api;
The file itself can look like:
namespace App\Http\Resources\Api;
use Illuminate\Http\Resources\Json\Resource;
/** #mixin \App\Models\Booking */
class BookingResource extends Resource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
$append = $request->get('append');
return [
// Here you'd put everything you want to show
'id' => $this->id,
'translator_email' => $append == 'translator_email' ? $this->translator_email : null,
// ...
];
}
}
In laravel API Resources:
I need a dynamic way to generalize a code for all resources to be used in all controllers instead of using resources in all methods for each controller .. for more clarification, I have a trait that includes generalized functions which return json responses with data and status code, lets take a "sample function" suppose it is showAll(Collection $collection) which is used for returning a collection of data of the specified model for example it is used for returning all users data ..
so I need to build a function that call what ever resource of the specified model, knowing that I have many models...
a) trait that include showAll method:
namespace App\Traits;
use Illuminate\Support\Collection;
trait ApiResponser
{
private function successResponse($data, $code) {
return response()->json($data, $code);
}
protected function showAll(Collection $collection, $code = 200) {
$collection = $this->resourceData($collection);
$collection = $this->filterData($collection);
$collection = $this->sortData($collection);
$collection = $this->paginate($collection);
$collection = $this->cacheResponse($collection);
return $this->successResponse([$collection, 'code' => $code], $code);
}
protected function resourceData(Collection $collection) {
return $collection;
}
}
b) usercontroller as a sample
namespace App\Http\Controllers\User;
use App\User;
use Illuminate\Http\Request;
use App\Http\Controllers\ApiController;
class UserController extends ApiController
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
$users = User::all();
// Here the showAll(Collection $collection) is used
return $this->showAll($users);
}
}
c) UserResource:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'identity' => $this->id,
'name' => $this->name,
'email' => $this->email,
'isVerified' => $this->verified,
'isAdmin' => $this->admin,
'createDate' => $this->created_at,
'updateDate' => $this->updated_at,
'deleteDate' => $this->deleted_at,
];
}
}
generalize: means used everywhere without code redundancy
What about providers, you may load data there and make that data reachable at places where user data can be reachable ?
laravel docs
I found a simple solution.. by adding the following method
protected function resourceData($collection) {
$collection = get_class($collection[0]);
$resource = 'App\Http\Resources\\' . str_replace('App\\', '', $collection) .
'Resource';
return $resource;
}
The $collection[0] in the first line of this method will get the
model you are currently using.
get_class will get the model name ex: App\User
'App\Http\Resources\\' . str_replace('App\\', '', $collection):
This will get the path of the resource by adding 'App\Http\Resources\' before the
model
str_replace('App\\', '', $collection): will remove App\ path from the collection
name so App\User should be User
then 'Resource' would be concatenated with the previous results and the whole
string should be like that: App\Http\Resources\UserResource
So at the end you should return the whole string App\Http\Resources\UserResource
,finally you should call the resourceData() in
the showAll() method:
protected function showAll(Collection $collection, $code = 200) {
$collection = $this->resourceData($collection);
$collection = $this->filterData($collection);
$collection = $this->sortData($collection);
$collection = $this->paginate($collection);
//Calling resourceData() method
$resource = $this->resourceData($collection);
$collection = $this->cacheResponse($collection);
return $this->successResponse([$resource::collection($collection), 'code' => $code], $code);
}
In my project, I have duplicated functions in my Controllers as each Controller needs to have similar functionality but it feels slightly dirty to repeat code.
In both my EventController and my ArticleController I have a duplicated function called handleTags() that literally does the same thing in each model.
The code looks like this:
/**
* Handle tagging of this resource
*
* First, get the tags field and make sure it isn't empty
* Convert the string into an array and loop through the array
* Create new tags if they don't exist
*
* #param Request $request: data from the request
* #param int $id: the ID of the model instance have tags synced to
*/
public function handleTags(Request $request, $id)
{
$event = Event::find($id);
if ($request->has('tags')) {
$tags = $request->get('tags');
if (!empty($tags)) {
// Turn a String into an array E.g. one, two
$tagArray = array_filter(explode(", ", $tags));
// Loop through the tag array that we just created
foreach ($tagArray as $tag) {
Tag::firstOrCreate([
'name' => ucfirst(trim($tag)),
'slug' => str_slug($tag)
]);
}
// Grab the IDs for the tags in the array
$tags = Tag::whereIn('name', $tagArray)->get()->pluck('id');
$event->tags()->sync($tags);
} else {
// If there were no tags, remove them from this model instance
$event->tags()->sync(array());
}
}
}
Would it be possible to move this functionality into a Trait? Something like Taggable?
Then you would call handleTags() in the relevant Controllers via the trait, in the same way Searchable gives you access to the search() method?
I think that a better solution will be to make a Model trait, I will explain my self.
trait HasTags {
public function handleTags($tags)
{
$tags = array_filter(explode(", ", $tags))
$tags = array_map(function () {
return Tag::firstOrCreate([
'name' => ucfirst(trim($tag)),
'slug' => str_slug($tag)
]);
}, $tags)
$this->tags()->sync(collect($tags)->pluck('id'))
}
public function tags()
{
return $this->morphMany(Tag::class);
}
}
Model
class Event extends Model
{
use HasTags;
}
Controller
$event = Event::find($id);
if ($request->has('tags')) {
$event->handleTags($request->get('tags'));
}
I Write it very quickly and without testing it but this is the general idea.
you can event have more refactoring by using all the array manipulations with collections.
You can make a trait in app/Http/Traits/TaggableTrait.php
You just need to pass an object instead of the id, so that the function will be independent from the class type.
then your trait will be something like this :
namespace App\Http\Traits;
use App\Tag;
trait TaggableTrait
{
/**
* #param Request $request: data from the request
* #param App\Article | App\Event $object: the model instance have tags synced to
*/
public function handleTags(Request $request, $object)
{
if ($request->has('tags')) {
$tags = $request->get('tags');
if (!empty($tags)) {
// Turn a String into an array E.g. one, two
$tagArray = array_filter(explode(", ", $tags));
// Loop through the tag array that we just created
foreach ($tagArray as $tag) {
Tag::firstOrCreate([
'name' => ucfirst(trim($tag)),
'slug' => str_slug($tag)
]);
}
// Grab the IDs for the tags in the array
$tags = Tag::whereIn('name', $tagArray)->get()->pluck('id');
$object->tags()->sync($tags);
} else {
// If there were no tags, remove them from this model instance
$object->tags()->sync(array());
}
}
}
}
EventController
use App\Http\Traits\TaggableTrait;
class EventController extends Controller
{
use TaggableTrait;
/*** ***** */
public function update(Request $request, $id)
{
/** ***/
$event = Event::findOrFail($id);
handleTags($request, $event);
/*** *** */
}
}
I would like to change the default mapping of the Product entity from the Sylius ProductBundle. So I created a listener to the ClassMetadata Event:
<?php
namespace App\Symfony\EventListener\Sylius;
use Sylius\Component\Product\Model\Product;
use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
/**
* Remove the reference of a Variant from its parent (Product) without delete it
*/
class ProductLoadMetadataSubscriber implements EventSubscriber
{
/**
* #return array
*/
public function getSubscribedEvents()
{
return array(
'loadClassMetadata',
);
}
/**
* #param LoadClassMetadataEventArgs $eventArgs
*/
public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs)
{
/** #var ClassMetadata $metadata */
$metadata = $eventArgs->getClassMetadata();
if (Product::class !== $metadata->name) {
return;
}
// Property "variants" in "Sylius\Component\Product\Model\Product" was already declared, but it must be declared only once
$metadata->mapOneToMany([
'fieldName' => 'variants',
'targetEntity' => 'Sylius\Component\Product\Model\VariantInterface',
'mappedBy' => 'object',
'orphanRemoval' => false
]);
//$variantsMapping = $metadata->getAssociationMapping('variants');
//$variantsMapping['orphanRemoval'] = false;
}
}
My goal is to set the orphanRemoval option value to false for the variants field.
But I didn't find a method or something else to update the ClassMetadataInfo instance.
At the moment I have this error:
Property "variants" in "Sylius\Component\Product\Model\Product" was
already declared, but it must be declared only once
It's logical because the variants field already exists.
EDIT:
I tried another thing but without success:
unset($metadata->associationMappings['variants']['orphanRemoval']);
// setAssociationOverride doesn't handle 'orphanRemoval' but it calls internally _validateAndCompleteOneToManyMapping
// which set orphanRemoval to false by default
$metadata->setAssociationOverride('variants', []);
To set orphanRemoval, you can try the following snippet:
if (SomeClass::class === $metadata->getName()) {
if (isset($metadata->associationMappings['itemShippingTrackings'])) {
$metadata->associationMappings['itemShippingTrackings']['orphanRemoval'] = false;
}
}