Yii2 prevent TimestampBehavior - php

I am creating a custom Identity interface without created_at property. I got an error :
"name": "Unknown Property",
"message": "Setting unknown property: api\\common\\models\\User::created_at",
I tried to comment the TimestampBehavior, but I got the following error:
"name": "PHP Warning",
"message": "Invalid argument supplied for foreach()",
I want to know where is the problem.
Model class:
class User extends ActiveRecord implements IdentityInterface
{
public static function tableName()
{
return '{{%user}}';
}
public function behaviors()
{
// return [
// TimestampBehavior::className(),
// ];
}
/**
* #inheritdoc
*/
public function rules()
{
return [
[['purpose'], 'required'],
[['status'], 'integer'],
];
}
}
for the rest controller the action is
public function actionLogin(){
.
.
.
$api_user = new User();
$api_user->purpose="app";
$api_user->status=User::STATUS_ACTIVE;
if($api_user->save()){
$success = true;
}
}

This will automatically resolve the issue. BlameableBehavior and TimestampBehavior
// Include these on the start
use yii\behaviors\BlameableBehavior;
use yii\behaviors\TimestampBehavior;
use Carbon\Carbon;
// Paste this function inside the class.
/**
* #return array
*/
public function behaviors()
{
return [
'blameable' => [
'class' => BlameableBehavior::className(),
'createdByAttribute' => 'created_by',
'updatedByAttribute' => 'updated_by',
],
'timestamp' => [
'class' => TimestampBehavior::className(),
'createdAtAttribute' => 'created_at',
'updatedAtAttribute' => 'updated_at',
'value' => Carbon::now(),
],
];
}
NOTE: If you are not using updated_at or updated_by then remove it
form the above code

change your Behavior in your model to:
public function behaviors()
{
return [
'timestamp' => [
'class' => 'yii\behaviors\TimestampBehavior',
'attributes' => [
ActiveRecord::EVENT_BEFORE_INSERT => ['updated_at'],
ActiveRecord::EVENT_BEFORE_UPDATE => ['updated_at'],
],
'value' => new Expression('NOW()'),
],
];
}
if you haven't updated_at also delete it from attributes.

You were getting following warning because you've completely removed the return in the behaviors() method.
"name": "PHP Warning",
"message": "Invalid argument supplied for foreach()",
The behaviors method must return an array. If you don't want to use any behavior your behaviors() method should return empty array like this:
public function behaviors()
{
return [];
}
This is also default implementation of behaviors() method in yii\base\Component so if you don't need to use any behavior you can simply remove the behaviors() method from your model.
Attaching TimestampBehavior to your model when you are not using it means that you add unnecessary overhead.

Example: Rename and prevent time recording or remove properties. Also change the value
Rename or delete properties or change value.
public function behaviors()
{
return [
[
'class' => \yii\behaviors\TimestampBehavior::className(),
'createdAtAttribute' => 'created_at',
// 'createdAtAttribute' => 'c_time', //Change the name of the field
'updatedAtAttribute' => false, //false if you do not want to record the creation time.
// 'value' => new Expression('NOW()'), // Change the value
],
];
}
Or
'class' => \yii\behaviors\TimestampBehavior::className(),
'attributes' => [
\yii\db\ActiveRecord::EVENT_BEFORE_INSERT => ['created_at'],
// \yii\db\ActiveRecord::EVENT_BEFORE_UPDATE => [],
],
$createdAtAttribute: The attribute that will receive timestamp value Set this property to false if you do not want to record the creation time.
$attributes: List of attributes that are to be automatically filled with the value specified via $value. The array keys are the ActiveRecord events upon which the attributes are to be updated, and the array values are the corresponding attribute(s) to be updated. You can use a string to represent a single attribute, or an array to represent a list of attributes. For example,
[
ActiveRecord::EVENT_BEFORE_INSERT => ['attribute1', 'attribute2'],
ActiveRecord::EVENT_BEFORE_UPDATE => 'attribute2',
]

Related

Laravel/React - "Call to a member function validated() on null" when creating a different FormRequest in Controller

I'm trying to validate two different types of data in a single axios call: Profile and ProfileSocial. The problem is, when I create a ProfileSocialRequest based on the second model and try to validate it, it returns Call to a member function validated() on null; if I return the data before attempting to validate it, it returns a valid object.
ProfileSocial
class ProfileRequest extends FormRequest
{
public function rules()
{
return [
'name' => [
'required',
'string',
],
...
'socials' => [
'required',
'array',
'min:1',
],
];
}
}
ProfileSocialRequest
use Illuminate\Foundation\Http\FormRequest;
class ProfileSocialRequest extends FormRequest
{
public function rules()
{
return [
'profile_id' => [
'required',
'integer',
],
'social_type_id' => [
'required',
'integer',
],
'handle' => [
'required',
'string',
],
];
}
}
ProfileController
public function store(ProfileRequest $request)
{
$data = $request->validated(); // this (ProfileRequest) works fine
$sosicalReq = new ProfileSocialRequest($request['socials'][0]); // This returns a valid object: {"social_type_id":1,"handle":"mySocialNetworkHandle","profile_id":1000}
$socialReqData = $sr->validated(); // ERROR: Call to a member function validated() on null
...
}
My question is, why is $socialReq being read as null when calling validated() if every step of the way it returns a complete object?
I'm not sure what you want to achieve, to but to manually create validation class instead of
$sosicalReq = new ProfileSocialRequest($request['socials'][0]);
you should use:
$sosicalReq = app()->make(ProfileSocialRequest::class);
but it will validate the whole input not just $request['socials'][0]

Laravel Error when referencing a resource in a resource

I'm using a Laravel Json Resource in my controller, as follows
public function index(Request $request)
{
$itemsWithTranslations = MenuItem::where(['menu_id' => $request->id, 'parent_id' => null])
->with(['children', 'translations'])
->orderBy('sort_order', 'asc')
->get();
return MenuItemResource::collection($itemsWithTranslations);
}
Now I would like to generate a collection, inside this collection with the children for the item that's being shown.
The following code works fine. Notice how I commented out the children reference
class MenuItemResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'text' => $this->title,
// 'children' => MenuItemResource::collection($this->whenLoaded('children')),
'data' => [
'id' => [
'value' => $this->id,
'type' => 'hidden'
],
'title' => [
'value' => $this->title,
'type' => 'text',
'label' => 'Title'
],
'resource_link' => [
'value' => $this->resource_link,
'type' => 'text',
'label' => 'Resource Link'
],
'translations' => MenuItemTranslationResource::collection($this->whenLoaded('translations'))->keyBy(function ($translation) {
return $translation['locale'];
})
]
];
}
}
When I uncomment the children, I get the following error
"Call to undefined method Illuminate\Http\Resources\Json\AnonymousResourceCollection::keyBy()"
Is it wrong, to include a Resource inside a resource? Or how should I go about this?
Model
class MenuItem extends Model
{
protected $table = 'menu_items';
protected $fillable = ['menu_id', 'parent_id', 'title', 'order', 'resource_link', 'html_class', 'is_blank'];
public function translations()
{
return $this->hasMany(MenuItemTranslation::class, 'menu_item_id');
}
public function children()
{
return $this->hasMany(MenuItem::class, 'parent_id');
}
}
Extra Information
When I return the following data, it does return empty as a collection for the children.
MenuItemResource::collection($this->children);
This returns
While if I return the children without a collection, it returns them (for 1 item, which is correct)
return $this->children;
returns
you should use ChildrenResource::collection
'children' => ChildrenResource::collection($this->whenLoaded('children'))
hope this works.
create a ChildrenResource class if not exists.

Saving multiple records in a laravel eloquent create

I'm trying to save multiple records via
AppSettings::create(
[
'name' => 'mail_host',
'type' => $emailsettingstype->id,
'value' => '',
],
[
'name' => 'mail_port',
'type' => $emailsettingstype->id,
'value' => '',
],
[
'name' => 'mail_username',
'type' => $emailsettingstype->id,
'value' => '',
],
);
But from the above, only the first array is getting created. Where am i going wrong? Any help is appreciated.
I think this should do
AppSettings::createMany([
[
'name'=>'mail_host',
'type'=>$emailsettingstype->id,
'value'=>'',
],
[
'name'=>'mail_port',
'type'=>$emailsettingstype->id,
'value'=>'',
],
[
'name'=>'mail_username',
'type'=>$emailsettingstype->id,
'value'=>'',
],
]);
Make sure you're passing an array of arrays, not a params of array.
UPDATE, you can use Model::insert() although according to what I've read, that method doesn't create/update the timestamps.
You can just use Eloquent::insert() link as below:
AppSettings::insert([
[
'name'=>'mail_host',
'type'=>$emailsettingstype->id,
'value'=>'',
],
[
'name'=>'mail_port',
'type'=>$emailsettingstype->id,
'value'=>'',
],
[
'name'=>'mail_username',
'type'=>$emailsettingstype->id,
'value'=>'',
],
]);
The problem with above is that it won't update timestamps, find examples here
The Create many Method createMany is available on relationship check reference to this link and this documentation from laravel
so far my example look like this.
I have two models Pricing and AvailableService Model
Pricing Model
namespace App;
use Illuminate\Database\Eloquent\Model;
class Pricing extends Model
{
protected $fillable = ["name", "price"];
public function available(){
return $this->hasMany(AvailableService::class, "pricing_id", "id");
}
}
And the AvailableServiceMode look like this
namespace App;
use Illuminate\Database\Eloquent\Model;
class AvailableService extends Model
{
protected $fillable = ["pricing_id", "service_id"];
public function service(){
return $this->belongsTo(Service::class, "service_id", "id");
}
}
So createMany operation look like this
$insertMany = Pricing::create(['name'=>request('name')]);
$insertMany->available()->createMany([
['service_id'=>1],
['service_id'=>2],
['service_id'=>3],
['service_id'=>4],
['service_id'=>5],
]);
And it works for, you can give it a try too. THANKS
If you want to store multiple record in seeder use this method instead of insert because in my case I want to slug automatically created using spatie/laravel-sluggable pkg. If you used the insert or DB technique then you have to give the value for slug field also.
CategorySeeder
<?php
namespace Database\Seeders;
use App\Servcategory;
use Illuminate\Database\Seeder;
class CategorySeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
$categories = [
[
'name' => 'Automotive',
// 'slug' => 'automotive',
],
[
'name' => 'Business Services',
// 'slug' => 'business-services',
],
[
'name' => 'Computer, Telecom & IT Services',
// 'slug' => 'computer-telecom-&-it-services',
],
[
'name' => 'Education & Training',
// 'slug' => 'education-&-training',
],
[
'name' => 'Finance',
// 'slug' => 'finance',
],
[
'name' => 'Hospitals, Clinic, Medical',
// 'slug' => 'hospitals-clinic-medical',
],
[
'name' => 'Real Estate, Construction, Property',
// 'slug' => 'real-estate-construction-property',
],
[
'name' => 'Travel,Toursim & Hotels',
// 'slug' => 'travel-toursim-&-hotels',
],
];
// Servcategory::insert($categories);
collect($categories)->each(function ($category) { Servcategory::create($category); });
}
}
In case some one searching for eloquent model, I used the following method:
foreach($arCategories as $v)
{
if($v>0){
$obj = new Self(); // this is to have new instance of own
$obj->page_id = $page_id;
$obj->category_id = $v;
$obj->save();
}
}
$obj = new Self(); is a must otherwise it only saves single record when $this is used.
in seeder create an array and do foreach with Model::create(). All your records will be with timestamps
protected $array = [
[...],
[...],
[...]
];
public function run()
{
foreach ($this->array as $value) {
Model::create($value);
}
}

Form with more than one collection

I want to realize a form, which is quite simple. The only thing that makes things complicated is that I 'm using two collections in my form. Displaying two collections in the view works like a charme. The problem is the validation and the associated hydration of the bound entity of the form. If all is validated and no errors occur the form instance tries to hydrate the bound entity and ends up with an exception:
Zend\Hydrator\ArraySerializable::hydrate expects the provided object to implement exchangeArray() or populate()
But first the example code ...
The form classes
namespace Application\Form;
use Zend\Form\Element\Collection;
use Zend\Form\Element\Text;
use Zend\Form\Form;
class MyForm extends Form
{
public function __construct($name = '', $options = [])
{
parent::__construct($name, $options);
$this->setAttribute('method', 'post');
$this->setAttribute('id', 'my-form');
}
public function init()
{
$this->add([
'name' => 'my-text-field',
'type' => Text::class,
'attributes' => [
...
],
'options' => [
...
],
]);
// The first collection
$this->add([
'name' => 'first-collection',
'type' => Collection::class,
'options' => [
'count' => 2,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => FieldsetOne::class,
],
],
]);
// the second collection
$this->add([
'name' => 'second-collection',
'type' => Collection::class,
'options' => [
'count' => 2,
'should_create_template' => true,
'template_placeholder' => '__index__',
'allow_add' => true,
'allow_remove' => true,
'target_element' => [
'type' => FieldsetTwo::class,
],
],
]);
}
}
The metioned Fieldset classes which are bound to the collections look pretty much the same.
namespace Application\Form;
use Zend\Form\Element\Number;
use Zend\Form\Fieldset;
use Zend\InputFilter\InputFilterProviderInterface;
class FieldsetOne extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'my-number',
'type' => Number::class,
'options' => [
...
],
'attributes' => [
...
],
]);
}
public function getInputFilterSpecification()
{
return [
'my-number' => [
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => ToInt::class,
],
],
'validators' => [
[
'name' => NotEmpty::class,
],
[
'name' => IsInt::class,
'options' => [
'locale' => 'de_DE',
],
],
],
],
];
}
}
Summed up the form got two collections of number elements. All data which is provided over the form should end up in the following entity.
The input filter class
The form gets filtered and validated by the following input filter. The input filter will be bound to the form via a factory. The factory will be shown later.
class MyFormInputFilter extends InputFilter
{
public function init()
{
$this->add([
'name' => 'my-text-field',
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => StringTrim::class,
],
],
]);
}
}
The input filter contains only settings for the my-text-field element. The collections will be validated with the implemented InputFilterProviderInterface in the fieldsets set as target elements. The input filter class is created over a factory and notated in the input_filters section in the module.config.php.
The form entity
The entity will be bound as an object to the form in a factory it looks like the following example.
namespace Application\Entity;
class MyFormEntity
{
protected $myTextField;
protected $firstCollection;
protected $secondCollection;
public function getMyTextField()
{
return $this->myTextField;
}
public function setMyTextField($myTextField)
{
$this->myTextField = $myTextField;
return $this;
}
public function getFirstCollection()
{
return $this->firstCollection;
}
public function setFirstCollection(array $firstCollection)
{
$this->firstCollection = $firstCollection;
return $this;
}
public function getSecondCollection()
{
return $this->secondCollection;
}
public function setSecondCollection(array $secondCollection)
{
$this->secondCollection = $secondCollection;
return $this;
}
}
This entity will be bound as object to the form. The form will be hydrated be zend 's own ClassMethods hydrator class. For the collections two hydrator strategies are added to the hydrator. The hydrator strategies for the collections look like this.
namespace Application\Hydrator\Strategy;
class FirstCollectionStrategy extends DefaultStrategy
{
public function hydrate($value)
{
$aEntities = [];
if (is_array($value)) {
foreach ($value as $key => $data) {
$aEntities[] = (new ClassMethods(false))->hydrate($data, new CollectionOneEntity());
}
}
return $aEntities;
}
}
This strategy will hydrate the data from collection one to the corresponding entity.
All wrapped up in a factory
This is the factory which creates the form instance.
class MyFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();
$filter = $parentLocator->get('InputFilterManager')->get(MyFormInputFilter::class);
$hydrator = (new ClassMethods())
->addStrategy('first-collection', new FirstCollectionStrategy())
->addStrategy('second-collection', new SecondCollectionStrategy());
$object = new MyFormEntity();
$form = (new MyForm())
->setInputFilter($filter)
->setHydrator($hydrator)
->setObject($object);
return $form;
}
}
This factory is mentionend in the form_elements section in the module.config.php file.
The problem
Everything works fine. The input element and also the collections are rendered in the view. If the form is submitted and the $form->isValid() method gets called in the controller all ends up in a BadMethodCallException.
Zend\Hydrator\ArraySerializable::hydrate expects the provided object to implement exchangeArray() or populate()
I have not bound the collection entities to the form in the controller because the hydrator strategies are added to the form hydrator that should hydrate the form entity. This makes sense for me, because zend form can only bind one object. If i call the bind method twice in the controller, the first bound object will be overwritten.
Is it possible to add more than one object with the bind method of the form so two collections can be handled? What could alternatives look like? What I 'm doing wrong?

ZF2 form element collection validation

So I have a "simple" form
class SiteAddForm extends Form
{
public function __construct()
{
parent::__construct('add_site_form');
$site = new SiteFieldSet();
$this->add($site);
}
public function getTemplate()
{
return 'site_add.phtml';
}
}
The form it self does nothing. It adds a field_set and returns a template name.
The SiteFieldSet looks likes:
class SiteFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('site');
$name = new Text('name');
$this->add($name);
$domains = new Collection('domains');
$domains->setTargetElement(new DomainFieldSet())
->setShouldCreateTemplate(true);
$this->add($domains);
}
public function getTemplate()
{
return 'site.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'name' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'domains' => [
'required' => true,
],
];
}
}
It adds a text and collection element to the fieldset. The field set implements InputFilterProviderInterface to validate the data thrown into it.
The name must be at least 200 chars (for testing) and the collection is required.
But now comes my problem. With the field set that is thrown into the collection, code:
class DomainFieldSet
extends FieldSet
implements InputFilterProviderInterface
{
public function __construct()
{
parent::__construct('domain');
$host = new Url('host');
$this->add($host);
$language = new Select('language', [
'value_options' => [
'nl_NL' => 'NL',
],
]);
$this->add($language);
$theme = new Select('theme', [
'value_options' => [
'yeti' => 'Yeti',
]
]);
$this->add($theme);
}
public function getTemplate()
{
return 'domain.phtml';
}
/**
* Should return an array specification compatible with
* {#link Zend\InputFilter\Factory::createInputFilter()}.
*
* #return array
*/
public function getInputFilterSpecification()
{
return [
'host' => [
'required' => true,
'validators' => [
new StringLength([
'min' => 200,
])
]
],
'language' => [
'required' => true,
],
'theme' => [
'required' => true,
],
];
}
}
Again nothing special. There are now three elements defined host, theme & language. Again the field set implements InputFilterProviderInterface. So there must be an getInputFilterSpecification in the class.
When I fill in the form
site[name] = "test"
site[domains][0][host] = 'test'
site[domains][0][theme] = 'yeti'
site[domains][0][language] = 'nl_NL'
It gives an error for site[name] saying it must be atleast 200 chars, so validations "works"
But it should also give an error on site[domains][0][host] that it needs to be atleast 200 chars (code was copy pasted, and the use is correct).
So why doesn't the validation kicks in, and or how can I solve the issue so a element/field set inside a collection is properly validated
Try using setValidationGroup in the form __construct method
like:
public function __construct()
{
$this->add(array(
'type' => 'Your\Namespace\SiteFieldSet',
'options' => array(
'use_as_base_fieldset' => true,
),
));
$this->setValidationGroup(array(
'site' => array(
'domain' => array(
'host',
'language',
'theme',
),
),
));
}
or this may also work...
$this->setValidationGroup(FormInterface::VALIDATE_ALL);

Categories