Exclude fields from a Resource template Laravel 7 and 8 - eloquent

I have come across a solution that filters fields from a resource collection in the Controller CompanyController.php
E.g The code below returns all the values except company_logo
CompanyResource::collection($companies)->hide(['company_logo']);
CompanyResource.php
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class CompanyResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected $withoutFields = [];
public static function collection($resource)
{
return tap(new CompanyResourceCollection($resource), function ($collection) {
$collection->collects = __CLASS__;
});
}
// Set the keys that are supposed to be filtered out
public function hide(array $fields)
{
$this->withoutFields = $fields;
return $this;
}
// Remove the filtered keys.
protected function filterFields($array)
{
return collect($array)->forget($this->withoutFields)->toArray();
}
public function toArray($request)
{
return $this->filterFields([
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'telephone' => $this->telephone,
'company_logo' => $this->company_logo,
'social_links' => $this->social_links,
]);
}
}
Now from my UserResource I still want to specify fields I don't want returned from the same CompanyResource but it's not a collection anymore in the UserResource
UserResource.php
public function toArray($request)
{
return [
'id' => $this->id,
'email' => $this->email,
'status' => $this->status,
'timezone' => $this->timezone,
'last_name' => $this->last_name,
'first_name' => $this->first_name,
'tags' => TagResource::collection($this->whenLoaded('tags')),
'company' => new CompanyResource($this->whenLoaded('company')),
];
}
So my idea is to be able to specify excluded fields on 'company' => new CompanyResource($this->whenLoaded('company')), Been stuck here for some time.

After researching I found a working solution for my problem
'company' => CompanyResource::make($this->whenLoaded('company'))->hide(['company_logo']),
Instead of the below which I could not use flexibly use:
'company' => new CompanyResource($this->whenLoaded('company')),

Related

new Model fills updated_at

I am using Lumen and I just found an issue. When creating a new model, the code also fills 'updated_at' despite the model is new and it wasn't updated yet (since it was just created). Since this is a crucial flaw and would be strange that it wasn't noticed till now, I presume I am doing something wrong.
App\User.php:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
const CREATED_AT = 'date_created';
const UPDATED_AT = 'date_updated';
protected $fillable = [
'name',
'email',
'role_id',
'password'
];
protected $hidden = [
'token',
'password',
'date_password_reset',
'token_password_reset'
];
protected $casts = [
'date_created' => 'datetime:Uv',
'date_updated' => 'datetime:Uv',
'date_password_reset' => 'datetime:Uv'
];
protected $with = ['userRoles'];
protected $validationRules = [...];
}
App\Http\Controllers\UsersController.php:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use App\User;
use Illuminate\Validation\ValidationException;
use Exception;
class UsersController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
...
// first attempt
public function create(Request $request) {
$this->validate($request, (new User)->rules('create'));
$user = new User;
$user->name = $request->input('name');
$user->email = $request->input('email');
$user->password = Hash::make($request->input('password'));
$user->role_id = $request->input('role_id');
$user->active = $request->boolean('active');
$user->save();
return response()->json(
[
'success' => true,
'message' => 'User successfully created',
'data' => User::query()->find($user->id)
], 200);
}
// second attempt
public function create(Request $request) {
$this->validate($request, (new User)->rules('create'));
$user = new User;
$user->update($request->input());
$user->password = Hash::make($request->input('password'));
$user->active = $request->boolean('active');
$user->save();
return response()->json(
[
'success' => true,
'message' => 'User successfully created',
'data' => User::query()->find($user->id)
], 200);
}
// third attempt
public function create(Request $request) {
$this->validate($request, (new User)->rules('create'));
$user = new User($request->input());
$user->password = Hash::make($request->input('password'));
$user->active = $request->boolean('active');
$user->save();
return response()->json(
[
'success' => true,
'message' => 'User successfully created',
'data' => User::query()->find($user->id)
], 200);
}
}
DB migration:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Hash;
class CreateUsersTable extends Migration
{
protected $initialUsers = [
[
'name' => 'Administrator',
'email' => 'admin#localhost.local',
'password' => null,
'role_id' => null,
'active' => 1
]
];
protected $initialRoles = [
[
'name' => 'Administrator'
],
[
'name' => 'User'
]
];
protected $initialUserRoles = [];
public function up()
{
DB::beginTransaction();;
Schema::create('users_roles', function (Blueprint $table) {
$table->id();
$table->string('name', 50);
$table->timestamp('date_created')->useCurrent();
$table->timestamp('date_updated')->nullable()->default(DB::raw('NULL ON UPDATE CURRENT_TIMESTAMP'));
});
foreach ($this->initialRoles as $data) {
DB::table('user_roles')->insert($data);
}
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name', 100);
$table->string('email', 255)->unique();
$table->string('password');
$table->unsignedBigInteger('role_id')->nullable();
$table->string('token', 255)->nullable();
$table->tinyInteger('active');
$table->timestamp('date_password_reset')->nullable();
$table->string('token_password_reset')->nullable();
$table->timestamp('date_created')->useCurrent();
$table->timestamp('date_updated')->nullable()->default(DB::raw('NULL ON UPDATE CURRENT_TIMESTAMP'));
$table->foreign('role_id')->references('id')->on('users_roles')->onDelete('set null')->onUpdate('cascade');
});
$password = Hash::make('xxx#x');
$role_id = DB::table('users_roles')->where('name', 'Administrator')->value('id');
foreach ($this->initialUsers as $data) {
$data['password'] = $password;
$data['role_id'] = $role_id;
$user_id = DB::table('users')->insertGetId($data);
$this->initialUserRoles[] = [
'user_id' => $user_id,
'role_id' => $role_id
];
}
DB::commit();
}
}
Query created by Eloquent:
array (size=3)
'query' => string 'insert into `users` (`name`, `email`, `password`, `role_id`, `active`, `date_updated`, `date_created`) values (?, ?, ?, ?, ?, ?, ?)' (length=131)
'bindings' =>
array (size=7)
0 => string 'Test User 5' (length=11)
1 => string 'test6#test.net' (length=14)
2 => string '$2y$10$lvRGKuznotd8lqwCj2diIONGjyiAkhaNthWGjQyFWbBqiyuf20wpG' (length=60)
3 => string '1' (length=1)
4 => boolean true
5 => string '2020-06-17 10:30:07' (length=19)
6 => string '2020-06-17 10:30:07' (length=19)
'time' => float 5.5
All three create() attempts are filling up 'updated_at' despite that one should stay NULL until an actual update is done on this model. Can you guys give me any indication what am I doing wrong? Would also like to keep the $model->update($request->input()) functionality if possible, so that I do not need too assign each field manually.
Because Lumen/Laravel is handling the "created_at" and "updated_at" fields on it's own and it sets both to the same timestamp when a record is created (which I do not like), I created my own solution.
First is already in place when creating a migration. I created my own timestamp fields and those are already properly updated by MySQL:
$table->timestamp('date_created')->useCurrent();
$table->timestamp('date_updated')->nullable()->default(DB::raw('NULL ON UPDATE CURRENT_TIMESTAMP'));
And of course, disable timestamp handling for Lumen in User.php model:
public $timestamps = false;
But in case that this is not possible for any reason or if you want Lumen/Laravel to handle those, this is the code I wrote in User.php model:
const CREATED_AT = 'date_created';
const UPDATED_AT = 'date_updated';
public $timestamps = false;
public static function boot() {
parent::boot();
static::creating(function ($model) {
$model->{self::CREATED_AT} = $model->freshTimestamp();
});
static::updating(function ($model) {
$model->{self::UPDATED_AT} = $model->freshTimestamp();
});
}

Zend ServiceManager using setter injection

in symfony i can use the setter injection for services via call option (https://symfony.com/doc/current/service_container/calls.html)
The example from the symfony documentation:
class MessageGenerator
{
private $logger;
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
}
// ...
}
service.yml
services:
App\Service\MessageGenerator:
# ...
calls:
- method: setLogger
arguments:
- '#logger'
I need this behaviour for my zend project. i want to inject a InputFilter into my FormFieldSet.
I didn't find anything about this in the zend documentation. Can i use something like this or exist a better solution for my problem in zend?
Based on this question and your previous question about Forms, Fieldsets and InputFilters, I'm thinking you want to achieve something similar to the following use case.
Use case
You have a
Location Entity
Address Entity
Location has a OneToOne to an Address (required, uni-directional)
Requirements
To manage the Location, you'll need:
LocationForm (-Factory)
LocationFormInputFilter (-Factory)
LocationFieldset (-Factory)
LocationFieldsetInputFilter (-Factory)
AddressFieldset (-Factory)
AddressFieldsetInputFilter (-Factory)
Configuration
To configure this in ZF3, you'll have to do add the following
'form_elements' => [
'factories' => [
AddressFieldset::class => AddressFieldsetFactory::class,
LocationForm::class => LocationFormFactory::class,
LocationFieldset::class => LocationFieldsetFactory::class,
],
],
'input_filters' => [
'factories' => [
AddressFieldsetInputFilter::class => AddressFieldsetInputFilterFactory::class,
LocationFormInputFilter::class => LocationFormInputFilterFactory::class,
LocationFieldsetInputFilter::class => LocationFieldsetInputFilterFactory::class,
],
],
Forms & Fieldsets
In the LocationForm, add your LocationFieldset and what else your Form needs, such as CSRF and submit button.
class LocationForm extends AbstractForm
{
public function init()
{
$this->add([
'name' => 'location',
'type' => LocationFieldset::class,
'options' => [
'use_as_base_fieldset' => true,
],
]);
//Call parent initializer. Adds CSRF & submit button
parent::init();
}
}
(Note: my AbstractForm does a bit more, I would suggest you have a look here, such as remove empty (child fieldsets/collections) Inputs so data is not attempted to be created in the DB)
In the LocationFieldset, give add Inputs for the Location, such as a name, and the AddressFieldset:
class LocationFieldset extends AbstractFieldset
{
public function init()
{
parent::init();
$this->add([
'name' => 'name',
'required' => true,
'type' => Text::class,
'options' => [
'label' => _('Name'),
],
]);
$this->add([
'type' => AddressFieldset::class,
'name' => 'address',
'required' => true,
'options' => [
'use_as_base_fieldset' => false,
'label' => _('Address'),
],
]);
}
}
In the AddressFieldset just add Inputs for the Address Entity. (Same as above, without the Fieldset type Input)
InputFilters
To validate the Form, you can keep it very simple:
class LocationFormInputFilter extends AbstractFormInputFilter
{
/** #var LocationFieldsetInputFilter */
protected $locationFieldsetInputFilter;
public function __construct(LocationFieldsetInputFilter $filter)
{
$this->locationFieldsetInputFilter = $filter;
parent::__construct();
}
public function init()
{
$this->add($this->locationFieldsetInputFilter, 'location');
parent::init();
}
}
(The AbstractFormInputFilter adds CSRF validator)
Notice that we simply ->add() the LocationFieldsetInputFilter, but we give it a name (2nd parameter). This name is used later in the complete structure, so it's important to both keep it simple and keep it correct. Simplest is to give it a name that one on one matches the object of the Fieldset it's supposed to validate.
Next, the LocationFieldsetInputFilter:
class LocationFieldsetInputFilter extends AbstractFieldsetInputFilter
{
/**
* #var AddressFieldsetInputFilter
*/
protected $addressFieldsetInputFilter;
public function __construct(AddressFieldsetInputFilter $addressFieldsetInputFilter)
{
$this->addressFieldsetInputFilter = $addressFieldsetInputFilter;
parent::__construct();
}
public function init()
{
parent::init();
$this->add($this->addressFieldsetInputFilter, 'address'); // Again, name is important
$this->add(
[
'name' => 'name',
'required' => true,
'filters' => [
['name' => StringTrim::class],
['name' => StripTags::class],
[
'name' => ToNull::class,
'options' => [
'type' => ToNull::TYPE_STRING,
],
],
],
'validators' => [
[
'name' => StringLength::class,
'options' => [
'min' => 3,
'max' => 255,
],
],
],
]
);
}
}
Factories
Now, you must bind them together, which is where your question about Setter injection comes from I think. This happens in the Factory.
A *FormFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$inputFilterPluginManager = $container->get('InputFilterManager');
$inputFilter = $inputFilterPluginManager->get(LocationFormInputFilter::class);
/** #var LocationForm $form */
$form = new LocationForm();
$form->setInputFilter($inputFilter); // The setter injection you're after
return $form;
}
A *FieldsetFactory would do the following (do the same for Location- and AddressFieldsets):
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var LocationFieldset $fieldset */
// name matters! Match the object to keep it simple. Name is used from Form to match the InputFilter (with same name!)
$fieldset = new LocationFieldset('location');
// Zend Reflection Hydrator, could easily be something else, such as DoctrineObject hydrator.
$fieldset->setHydrator(new Reflection());
$fieldset->setObject(new Location());
return $fieldset;
}
A *FormInputFilterFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$inputFilterPluginManager = $container->get('InputFilterManager');
/** #var LocationFieldsetInputFilter $locationFieldsetInputFilter */
$locationFieldsetInputFilter = $inputFilterPluginManager->get(LocationFieldsetInputFilter::class);
// Create Form InputFilter
$locationFormInputFilter = new LocationFormInputFilter(
$locationFieldsetInputFilter
);
return $locationFormInputFilter;
}
A *FieldsetInputFilterFactory would do the following:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
/** #var AddressFieldsetInputFilter $addressFieldsetInputFilter */
$addressFieldsetInputFilter = $this->getInputFilterManager()->get(AddressFieldsetInputFilter::class);
$addressFieldsetInputFilter->setRequired(true);
return new LocationFieldsetInputFilter(
$addressFieldsetInputFilter
);
}
Note:
Setting an InputFilter as (not) required is something I've added here
If your InputFilter (such as AddressFieldsetInputFilter) does not have a child InputFilter, you can can skip getting the child and straight away return the new InputFilter.
I think I covered it all for a complete picture. If you have any questions about this, please comment.
What you need are Initializers from Zend Service Manager.
The initializer can be a class that is called whenever a service has been created.
In that class, you need to check the type of service that is created, and if it's appropriate type than inject whatever you want.
To register one Initializer add in config under service_manager key:
'service_manager' => [
'initializers' => [
MyInitializer::class
],
]
and then just create that class
class MyInitializer implements InitializerInterface
{
public function __invoke(ContainerInterface $container, $instance)
{
// you need to check should you inject or not
if ($instance instanceof MessageGenerator) {
$instance->setLogger($container->get('logger'));
}
}
}
You need to have registred MessageGenerator in zend-servicemanager also. In this way, when you try to retrive MessageGenerator from SM, after creation MyInitializer is called.

Symfony Form Querybuilder with parameters

I have the need to create a dropdown field with grouped data:
My form:
class RetailerDetailFilterType extends AbstractType
{
public function getActiveRetailerMetrics(): array
{
return range(25,36);
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('month', EntityType::class,
[
'class' => 'AppBundle:ConsolidatedOperatorCategoryLowData',
'query_builder' => function(ConsolidatedOperatorCategoryLowDataRepository $er){
return $er->getMinMaxByMetricQueryBuilder($this->getActiveRetailerMetrics());
}
]);
}
public function getBlockPrefix()
{
return 'key_metric';
}
}
My Repository:
class ConsolidatedOperatorCategoryLowDataRepository extends \Doctrine\ORM\EntityRepository
{
public function getMinMaxByMetricQueryBuilder($metricRange)
{
$qb = $this->getEntityManager()
->createQueryBuilder('d');
$qb
->select('d.id, YEAR(d.date) as dyear, MONTH(d.date) as dmonth')
->from('AppBundle:ConsolidatedOperatorCategoryLowData','d')
->where($qb->expr()->in('d.metric_id', $metricRange))
->groupBy('dyear')
->addGroupBy('dmonth')
->setMaxResults(10)
;
return $qb;
}
I'm getting
Warning: spl_object_hash() expects parameter 1 to be object, integer given
at UnitOfWork ->isScheduledForInsert (3182005)
in vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php at line 710 +
at EntityManager ->contains (3182005)
in vendor/symfony/symfony/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php at line 116 +
at IdReader ->getIdValue (3182005)
at call_user_func (array(object(IdReader), 'getIdValue'), 3182005)
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 205 +
at ArrayChoiceList ->flatten (array('id' => 3182005, 'dyear' => '2016', 'dmonth' => '12'), array(object(IdReader), 'getIdValue'), array(), array(), null)
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 200 +
at ArrayChoiceList ->flatten (array(array('id' => 3182005, 'dyear' => '2016', 'dmonth' => '12'), array('id' => 3186685, 'dyear' => '2017', 'dmonth' => '1'), array('id' => 3191365, 'dyear' => '2017', 'dmonth' => '2'), array('id' => 3195595, 'dyear' => '2017', 'dmonth' => '3'), array('id' => 3200275, 'dyear' => '2017', 'dmonth' => '4')), array(object(IdReader), 'getIdValue'), array(), array(), array(null))
in vendor/symfony/symfony/src/Symfony/Component/Form/ChoiceList/ArrayChoiceList.php at line 91
I think it has to do with how Doctrine ORM deals with relationships. When you want to retrieve an entity by a referenced object's id, say find all posts by a user id, then you still have to pass the User-object to the QueryBuilder, not just the id. This is because Doctrine will resolve how those entities are connected itself.
It seems that metric_id actually is a reference to some kind of Metric-entity and just passing an array of int instead of the actual objects seems to trip up Doctrine's QueryBuilder.
You could try mapping the id's to a new instance of Metric and then pass that array instead.
Another solution - the one I would prefer - is to use Native SQL for this.

How to get NULL as an option in a datagrid relation in sonata admin bundle?

I added the following to a Sonata admin in order to filter by category. However, the list does not show NULL as an option for category. I want also want to be able to filter by category for when category is NULL instead of an entity.
How can one achieve this? My current configuration:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add("category");
}
Try this:
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper->add("category", 'doctrine_orm_callback', array(
'callback' => function ($queryBuilder, $alias, $field, $value) {
/**
* #var QueryBuilder $queryBuilder
*/
if ($value['value']) {
if ($value['value'] == 0) {
$queryBuilder->andWhere($queryBuilder->expr()->isNull($alias.'.category'));
return true;
} else {
$category = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager')->getReference('AcmeBundle:Category', $value['value']);
$queryBuilder->andWhere($queryBuilder->expr()->eq($alias.'.category', $category));
return true;
}
}
},
'field_type' => 'choice',
'field_options' => array(
'choices' => $this->getCategoryChoices()
),
'label' => 'Category'
));
}
private function getCategoryChoices()
{
$categories = $this->getConfigurationPool()->getContainer()->get('doctrine.orm.entity_manager')->getRepository('AcmeBundle:Category')->findAll();
$choices["0"] = "NULL";
foreach($categories as $category) {
$choices["{$category->getId()}"] = $category->getName();
}
return $choices;
}

Symfony2: How to add form constraint for a field in bind PRE_SET_DATA depending on the data

I have a form in Symfony 2 with basically two fields:
public function buildForm(FormBuilderInterface $builder, array $options) {
$builder->add('contactType', 'select', array( 'choices' => $contactTypes ))
->add('value', 'text');
}
Then I added an EventSubscriber that listens to the FormEvents::PRE_SET_DATA event. What I actually want to do, is to change the way of validation depending on the value of contactType (numeric values from 1 to 4, which stand for email, mobile, fixed line and fax).
I followed this tutorial http://symfony.com/doc/current/cookbook/form/dynamic_form_generation.html
but I can't figure out, how to add a constraint to the value field.
Can anyone help me? Thanks a lot in advance.
Instead of adding validation constraints dynamically in event subscriber (not sure if this is even possible), you can set groups to field's validation constraints and determine validation groups based on submitted data.
A function to create the form from the controller :
<?php
// ...
class DefaultController extends Controller
{
/**
*
* #param \Clicproxy\DeltadocCabBundle\Entity\Mark $mark
* #param \Clicproxy\DeltadocCabBundle\Entity\Tag $tag
* #return Form
*/
private function createTagForm(Mark $mark, Tag $tag)
{
$form = $this->createForm(new TagType(), $tag, array(
'action' => $this->generateUrl('tag_new', array('slug' => $this->slugify($mark->getName()))),
'method' => 'POST',
));
foreach ($mark->getFields() as $field)
{
$form->add($this->slugify($field->getName()), $field->getFormType(), $field->getOptions());
}
$form->add('submit', 'submit', array('label' => 'crud.default.save'));
return $form;
}
// ...
The code in the entity (type, constraints, ...) :
<?php
// ...
/**
* Field
*
* #ORM\Table()
* #ORM\Entity
* #UniqueEntity({"name", "mark"})
*/
class Field
{
// ...
/**
*
* #return array
*/
public function getOptions()
{
$options = array('label' => $this->getName(), 'mapped' => FALSE);
$options['required'] = $this->getType() != 'checkbox';
if ('date' == $this->getType())
{
$options['attr']['class'] = 'datepicker'; // 'input-group date datepicker';
$options['attr']['data-date-format'] = 'dd/mm/yyyy';
$options['attr']['data-date-autoclose'] = true;
}
if ('choice' == $this->getType())
{
$choices = array();
foreach ($this->getChoices() as $choice)
{
$choices[$choice->getValue()] = $choice->getName();
}
asort($choices);
$options['choices'] = $choices;
}
$options['constraints'] = $this->getValidationConstraint();
return $options;
}
public function getValidationConstraint ()
{
$validation_constraint = array();
if ('number' == $this->getType()) {
if (0 < $this->getMaximum()) {
$validation_constraint[] = new LessThanOrEqual (array(
'message' => 'entity.field.number.lessthanorequal', // {{ compared_value }}
'value' => $this->getMaximum()
));
}
if (0 < $this->getMinimum()) {
$validation_constraint[] = new GreaterThanOrEqual(array(
'message' => 'entity.field.number.greaterthanorequal', // {{ compared_value }}
'value' => $this->getMinimum()
));
}
} elseif ('text' == $this->getType ()) {
if (0 < $this->getMaximum()) {
$validation_constraint[] = new Length(array(
'min' => $this->getMinimum() > 0 ? $this->getMinimum() : 0,
'max' => $this->getMaximum() > 0 ? $this->getMaximum() : 0,
'minMessage' => 'entity.field.text.minMessage', // {{ limit }}
'maxMessage' => 'entity.field.text.maxMessage',
'exactMessage' => 'entity.field.text.exactMessage',
));
}
} elseif ('date' == $this->getType()) {
}
return $validation_constraint;
}
// ...
All this code work actually.
With this you have a solution to generate a form on the fly with constraints.