Fractal transformer with different Serializers for nested items and collections - php

To change the JSON output send by a Laravel 5.4 RESTful API, I make use of the Fractal package by thephpleague. Because pagination will be added in the future, it is important that collections make use of the default DataArraySerializer and single items use the ArraySerializer. It is also needed that deeper nested objects are given the same structure. How can I achieve this (globally or not)?
class TreeTransformer extends TransformerAbstract {
protected $defaultIncludes = [
'type',
'branches'
];
public function transform(Tree $tree) {
return [
'id' => (int)$tree->id,
'name' => (string)$tree->name
];
}
public function includeType(Tree $tree) {
return $this->item($tree->type, new TypeTransformer()); // Should be ArraySerializer
}
public function includeBranches(Tree $tree) {
return $this->collection($tree->branches, new BranchTransformer()); // Should stay DataArraySerializer
}
}

Unfortunately, I think what you are trying to do is not possible yet. More information here: https://github.com/thephpleague/fractal/issues/315
You can still change the serializer for an entire output like this: https://github.com/spatie/fractalistic#using-a-serializer. But it is not what you want to achieve.

Actually you can.
It may look quite more verbose, but the trick is just not using $defaultIncludes. Use the fractal helper instead.
class TreeTransformer extends TransformerAbstract {
public function transform(Tree $tree) {
return [
'id' => (int)$tree->id,
'name' => (string)$tree->name,
'type' => $this->serializeType($tree)
'branches' => $this->serializeBranches($tree)
];
}
private function serializeType(Tree $tree) {
return fractal()
->serializeWith(new ArraySerializer())
->collection($tree->type)
->transformWith(TypeTransformer::class)
->toArray(); // ArraySerializer
}
private function serializeBranches(Tree $tree) {
return fractal()
->serializeWith(new DataArraySerializer())
->collection($tree->branches)
->transformWith(BranchesTransformer::class)
->toArray(); // DataArraySerializer
}
}
It's working for me with ArraySerializer. Didn't try DataArraySerializer.

Related

Laravel - hasOne relation based on model attribute

I have a problem, I need to make a relation based on an attribute from the model$this->type, my code is as follows:
class Parent extends Model {
protected $attributes = [
'type' => self::TYPE_A
];
public function child()
{
return match ($this->type) {
self::TYPE_A => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_A),
self::TYPE_B => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_B),
self::TYPE_C,
self::TYPE_D => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_C),
self::TYPE_E,
self::TYPE_F => $this->hasOne(Cousin::class)->where('type', Cousin::TYPE_D)
};
}
public function children()
{
return $this->hasMany(Cousin::class);
}
}
If I do this inside artisan tinker, I get the child relation correctly:
$parent = Parent::first();
$parent->child;
However if my queues execute the code the child relation is returned as null.
If I put logger($this) in the first line of the relation I get an empty model when inside queues:
[2021-09-29 23:51:59] local.DEBUG: {"type":1}
Any idea on how to solve this?
Apparently it is not possible to use the relationship in this way.
The relationship I want uses composite keys and Eloquent doesn't support it.
They created a package to enable this to happen: https://github.com/topclaudy/compoships
But I don't want to add any more packages to my application as it is already too big.
My solution was to add one more column in the cousins table to know which one to use:
public function child() {
return $this->hasOne(Cousin::class)
->where('main', true);
}
protected $attributes = [
'type'
];
public function getTypeAttribute() {
return $this->hasOne(Cousin::class)
->where('main', true);
}
Then call it anywhere
$parent->type
Or you can do it as follow
public function typeMainTrue() {
return $this->hasOne(Cousin::class)
->where('main', true);
}
Then call it anywhere
$parent->typeMainTrue

Make Laravel's notIn validation rule case insensitive

I am storing an array of strings in my database (db column type is JSON). There is a form that allows users to add a value to this array. I want to make sure there are no duplicates in this array. The notIn validation rule appears be the simplest solution to prevent duplicates but it is case sensitive. So when using notIn I am not able to prevent identical strings that have different capitalization.
$this->validate(request(), [
'choice' => [
'required',
Rule::notIn($choices)
]
]);
Does anyone have recommendation on how I should fix this validation so that the string comparison is case insensitive?
You could lowercase your input data as well as your current data like this:
$input = request()->all();
$input['choice'] = array_map("strtolower", $input['choice']);
request()->validate($input, [
'choice' => [
'required',
Rule::notIn(array_map("strtolower", $choices))
]
]);
Thanks Ramy Herria, I was able to expand his answer to also work in a FormRequest class:
protected function validationData()
{
$all = parent::validationData();
//Convert request value to lowercase
$all['choice'] = strtolower($all['choice']);
return $all;
}
public function rules()
{
$choices = $this->route('modelName')->choices;
return [
'choice' => [
'required',
//Also convert array to lowercase
Rule::notIn(array_map('strtolower', $choices))
]
];
}
You can write your own validation rule class:
use Illuminate\Contracts\Validation\Rule;
use Illuminate\Validation\Concerns\ValidatesAttributes;
use Illuminate\Validation\Rules\In;
class CaseInsensitiveInRule extends In implements Rule
{
use ValidatesAttributes;
private const FORMAT_FUNCTION = 'strtoupper';
public function __construct(array $values)
{
$this->values = array_map(self::FORMAT_FUNCTION, $values);
}
public function passes($attribute, $value)
{
$value = call_user_func(self::FORMAT_FUNCTION, $value);
return $this->validateIn($attribute, $value, $this->values);
}
public function message()
{
return __('validation.invalid_value');
}
}
and next you can create a object in your request class
public function rules(): array
{
return [
'status' => new CaseInsensitiveInRule(['active', 'deleted'])
];
}
i know it is a little late, but for others i will suggest to use prepareForValidation method inside custom request class; like follow
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class RegisterRequest extends FormRequest
{
protected function prepareForValidation()
{
$this->merge([
'choices' => strtolower($this->choices),
]);
}
}
this way user input for choices are always lower case and the request itself is modified too.

Fractal Transformers Optional Model Columns

I am using Fractal Transformers in Laravel 5. I have:
namespace App\Transformers;
use App\Models\Cake;
use League\Fractal\TransformerAbstract;
class CakeTransformer extends TransformerAbstract
{
protected $availableIncludes = [
'user',
'description'
];
public function transform(Cake $cake)
{
$ar = [
'name' => $cake->name,
'url_name' => $cake->url_name,
'user' => $cake->user->screenname,
'date_created' => $cake->created_at
];
return $ar;
}
public function includeUser(Cake $cake)
{
return $this->item($cake->user, new UserTransformer());
}
public function includeDescription(Cake $cake) {
return $cake->description;
}
}
The above doesn't work because includeDescription doesn't return the right kind of object, but from the above you can see what I'm trying to do.
For instance in my search I want to bring back much less data than if I were to load a whole page about the search item. E.g. for search I don't want to load the description, but for the page that contains details about the product I would want to.
How can I achieve this?

Symfony: how to assign data to a template in an inheritating controller-action?

I have TheParentController and the inheritating TheChildController, which should assign $moreData to the template, but the render() method should be called in TheParentController.
Is there any function/service for this case? I expect anything like
$this->get('templating')->assignDataForTemplate('moreData', $moreData);
class TheParentController
{
public function myAction($param1) {
return $this->render('template.html.twig', array(
'someData' => $someData
));
}
}
-
class TheChildController
{
public function myAction($param1) {
// !
// Is there any function like "assignDataForTemplate"?
$this->get('templating')->assignDataForTemplate('moreData', $moreData);
// /!
return parent::myAction($param1);
}
}
I would like to avoid something like
// ...
public function myAction($param1, $moreData = null) {
return $this->render('template.html.twig', array(
'someData' => $someData,
'moreData' => $moreData
));
}
}
As far as I'm aware, there is no such way to do this currently. If you go through the sources, you'll see that calling $templating->render() is actually calling TwigEngine->render().That calls Twig_Template->render() which outputs the template to the client.
I fully understand why you might be using HMVC but I believe this approach might be overcomplicating things for you. If you have common code between controllers - just create a static class which can be called directly. Afterwards move your common logic/code there and call it whenever you need it.
Otherwise, you might need to stick with the code you're trying to avoid (or similar workaround) for now.
You could try something like this, so that the parent is unaware of the child.
<?php
class TheParentController {
public function myAction () {
$data = $this->getMyActionData();
return $this->render('template', $data);
}
protected function getMyActionData () {
return [
'someDefault' => 5
];
}
}
class TheChildController extends TheParentController {
// If using annotation based routing override myAction
// with call to parent function and new #Route tag in doc block
protected function getMyActionData () {
$parentData = parent::getMyActionData();
return array_merge($parentData, [
'childData' => 11
]);
}
}

Yii2 envelope single data in JSON response

I went through offical guide and found a way to envelop JSON data like this.
use yii\rest\ActiveController;
class UserController extends ActiveController
{
public $modelClass = 'app\models\User';
public $serializer = [
'class' => 'yii\rest\Serializer',
'collectionEnvelope' => 'items',
];
}
This works perfect when I have a collection and then I have a response like this.
{
products:....
}
But what I want to do is that i have a envelope for single data. For example if I do products/10 GET request to get.
{
product:
}
Hope somebody figured it out.
Single Data Envelope is not supported by \yii\rest\Serializer. At least until Yii 2.0.6 only collections get enveloped in order to add _links and _meta data objects to the response.
To envelope single data resource objects you'll need to override ActiveController's default view action within your Controller :
public function actions()
{
$actions = parent::actions();
unset($actions['view']);
return $actions;
}
public function actionView($id)
{
$model = Product::findOne($id);
return ['product' => $model];
}
Old, but I just bumped into here with the same problem.
And found a better (I think) solution: create your own serializer class extending \yii\rest\Serializer:
class Serializer extends \yii\rest\Serializer
{
public $itemEnvelope;
public function serializeModel($model)
{
$data = parent::serializeModel($model);
if($this->itemEnvelope)return [$this->itemEnvelope=>$data];
return $data;
}
}
And then use it like this:
public $serializer = [
'class' => '[your-namespace]\Serializer',
'collectionEnvelope' => 'list',
'itemEnvelope' => 'item'
];

Categories