Passing in multiple attributes when using Stand alone validators in Yii2 - php

I'm wanting to use a standalone validator in Yii2 but I'm not sure how pass multiple attributes in when overiding the validateAttributes().
Here is an example of my validator:
class RegistrationValidator extends Validator {
public function validateAttributes($model, $attributes = null) {
}
}
Here is how I am calling it from within the rules() method inside my model:
[['username','email'], RegistrationValidator::className()],
However when doing a var_dump inside the validator on the attributes variable I seem to have all the attribute names, not just username & email.
What am I doing wrong here?

You have overriden wrong method. You should override validateAttribute($model, $attribute) instead of validateAttributes($model, $attributes = null). At least this is the common way to do it.
You can access the attributes of the model with the parameter (as you have found out with validateAttributes()). However, you then have to decide which error messages the attributes should get. You can distinguish with the $attribute parameter. But you don't have to add errors to both attributes, although it could be reasonable.

Related

Retrieve fillable fields in Laravel

In laravel 5.4, I'm able to retrieve fillable fields by using fillable index of model instance.
$model = new AnyClass();
dd($model['fillable']);
The above code prints all fillable fields of AnyClass. But the same code prints null on laravel 5.6. I know I can retrieve fillable fields using $model->getFillable(). My question is what is the reason / why it is not working in laravel 5.6 but works in 5.4?
From the upgrade guide here I believe this is the answer to the question:
Model Methods & Attribute Names
To prevent accessing a model's private properties when using array access, it's no longer possible to have a model method with the same name as an attribute or property. Doing so will cause exceptions to be thrown when accessing the model's attributes via array access ($user['name']) or the data_get helper function.
If you look at Laravel's source code you'll see the difference.
The Model class, which is extended by the application models, implements the ArrayAccess interface, which, among others, force the class to define the offsetGet method.
In Laravel 5.4 the offsetGet method looks like:
public function offsetGet($offset)
{
return $this->$offset;
}
which means that if you call $model['fillable'], you actually call $model->offsetGet('fillable') which actually returns the fillable property of the class.
I couldn't find the Laravel 5.6 tag but I'm pretty sure it is the same code as Laravel 5.5.45. In this version the offsetGet method was changed to:
public function offsetGet($offset)
{
return $this->getAttribute($offset);
}
which means that it actually returns the attribute if found or null otherwise.
In Laravel 7, I'm doing this by calling the getFillable method on a new instance of my model. Like so:
$model = new MyModel();
$fillable = $model->getFillable();
Late to the party but I don't like the concept of having to always instance a Model, specially if you're using Eloquent serialization.
Let's say you wanted to build some filters, but wanted to whitelist the columns based on the model's fillable. You don't want to instance an entire model, so you can instead use reflection:
(new ReflectionClass(MyModel::class))->getDefaultProperties()['fillable']
See it working over at 3v4l.org - Here I demonstrate why you potentially wouldn't want to instance this model due to having serialization and always eager loading.
Change the property in the class to public $fillable = [ instead of protected $fillable = [

Symfony Validator Component issue in Standalone Applicatin

I am realizing that perhaps the way I want to make use of the Validator Component from Symfony is not possible. Here is the idea.
I have a class called Package which for now has only one property named namespace. Usually I would include the ClassMetadata and any constraint object I would like to validate against within my Package class. However, my idea is that instead of doing that I would rather keep my subject clean and only responsible for the things it must be responsible for.
Below is a class I wrote and call it PackageValidater:
<?php
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;
class PackageValidator
{
protected $subject;
public function PackageValidator($subject){
$this->subject = $subject;
}
public static function loadMetadata(){
$metadata->addPropertyConstraint('namespace', new new Assert\Type(['type' => 'string']));
}
public function getViolations(){
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadMetadata')
->getValidator();
$violations = $validator->validate($this->subject);
return !empty($violations) ? $violations : [];
}
}
Despite of the fact that I am not sure about the usage of my constraint since most reference uses annotations and I do not we can ignore that part. I also am aware of the fact that my test fails due to this fact. However, my issue is with my design because I have not added the static function that the Validation object uses to build the validation. Instead of my method mapping where constraints reside being in the actual object it resides on a separate class.
The idea is to enforce separation of concerns and single responsibility on my objects. Below is a diagram that depicts exactly what I am trying to achieve:
I have written my test as shown below:
$packageValidator = new PackageValidator(new Package([0 => 'test']));
$this->assertTrue(true, empty($packageValidator->getViolations()));
Above I have passed in an array instead of a string which would make my test fail because there can never be a single namespace that is in a form of array - at least not in what I am trying to achieve.
The issue is with my getViolations method inside the PackageValidator object because I am not passing my subject outside the context of my validation process that is define the subject metadata inside the subject itself then when getting the validator object with the refence to the subject's metadata get the validation errors.
All in all Package does not have loadMetadata method but PackageValidator. How can I make this possible without polluting every object I want to validate with the metadata functionality?
Below is what I get from PHPUnit:
SimplexTest\Validate\Package\PackageValidatorTest::testIfValidatorInterfaceWorks
Symfony\Component\Validator\Exception\RuntimeException: Cannot
validate values of type "NULL" automatically. Please provide a
constraint.
You can use yml or xml configuration to add constraints to your object.
http://symfony.com/doc/current/book/validation.html#the-basics-of-validation
You do this by creating a file called validation.yml in your Bundle configuration directory. Add the following content to validate your object:
Some\Name\Space\Package:
properties:
name:
- NotBlank: ~
That's one way to keep things you don't consider a responsibility for your object out of said object. It also removes the need for a custom validator class for every object you create. You can simply make use of the validator service already provided by the framework.
Edit
Alright I think I figured something out you might be looking for: you can create a MetadataFactory to load Metadata the way you want. There are a couple of examples here: https://github.com/symfony/validator/tree/master/Mapping/Factory
It basically boils down to a Factory class that returns an instance of MetadataInterface where you attach your constraints. This means that you can have the Factory read metadata from anything. You could for example do something like this:
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
use Your\Package;
class PackageMetadataFactory implements MetadataFactoryInterface
{
/**
* Create a ClassMetaData object for your Package object
*
* #param object $value The object that will be validated
*/
public function getMetadataFor($value)
{
// Create a class meta data object for your entity
$metadata = new ClassMetadata(Package::class);
// Add constraints to your metadata
$metadata->addPropertyConstraint(
'namespace', new Assert\Type(['type' => 'string']));
// Return the class metadata object
return $metadata;
}
/**
* Test if the value provided is actually of type Package
*
* #param object $value The object that will be validated
*/
public function hasMetadataForValue($value)
{
return $value instanceof Package::class;
}
}
Then in your PackageValidator all you have to do is:
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Validation;
use Your\PackageMetadataFactory;
class PackageValidator
{
protected $subject;
public function PackageValidator($subject) {
$this->subject = $subject;
}
public function getViolations() {
$validator = Validation::createValidatorBuilder()
->setMetadataFactory(new PackageMetadataFactory())
->getValidator();
$violations = $validator->validate($this->subject);
return !empty($violations) ? $violations : [];
}
}
Hopefully this is more in line of what you're looking for.
I have followed your suggestion above as you have put it. The only thing I had to change was the hasMetadaFor method implementation inside the PackageMetadataFactory. Below is how I rather check for property existence.
public function hasMetadataFor( $value ){
return property_exists(Package::class, $value);
}
Everything else as you suggested works perfectly. Below is my test function.
$validator = new PackageValidator(new OrderPackage(125787618));
$this->assertSame(true, $validator->validates());
The test fails because the namespace cannot be numbers. Passing the fully qualified class name of the OrderPackage by doing OrderPackage ::class validates the object.
Thank you very much for your advice.

Laravel: Access Model instance in Form Request when using Route/Model binding

I have some route/model binding set up in my project for one of my models, and that works just fine. I'm able to use my binding in my route path and accept an instance of my model as a parameter to the relevant method in my controller.
Now I'm trying to do some work with this model, so I have created a method in my controller that accepts a Form Request so I can carry out some validation.
public function edit(EditBrandRequest $request, Brand $brand)
{
// ...
Each different instance of my model can be validated differently, so I need to be able to use an instance of the model in order to build a custom set of validation rules.
Is there a way of getting the instance of the model, that is injected into the controller from the Form Request?
I have tried type-hinting the model instance in the Form Request's constructor
class EditBrandRequest extends Request
{
public function __construct(Brand $brand)
{
dd($brand);
}
I have also tried type-hinting the model instance in the Form Request's rules() method.
class EditBrandRequest extends Request
{
// ...
public function rules(Brand $brand)
{
dd($brand);
In both instances I am provided an empty/new instance of the model, rather than the instance I am expecting.
Of course, I could always get around this by not bothering with Form Requests and just generate the rules in the controller and validate manually - but I would rather do it the Laravel way if it's possible.
Thanks
You can simply access it using the binding key, so for example if you bind Brand model: $router->model('brand', '\App\Brand') you can get instance of your model with $this->brand. Here is validation rules example:
'slug' => 'required|unique:brand,slug,' . $this->brand->id,
EDIT
Sometimes you might have an input name that uses the same name as the binding key, for example, if you bind Address model as address then you have an input field address it will make Laravel confuse. For this situation you can use route() method.
'address' => 'required|unique:addresses,address,' . $this->route('address')->id,

Laravel accessing collection attributes and model attributes from reusable view

I have a view that I'm trying to re-use for two different actions to display data from the database. For one of those actions, an Eloquent collection object is passed to the view, and data is retrieved with
#foreach($buildings as $key=>$value)
{!! $value->build_name !!}
Obviously 'build_name' is a column in the table. So far simple..
Now I need this same view to display data that requires a lot of processing and it's not possible to generate an eloquent statement to pass to the view.
In order to re-use the $value->build_name code, I'm assuming I have to still pass an object (model??) to the view.
I have a Building.php Model
class Building extends Model
{
protected $fillable =[
'buildingtype_id',
'build_name',
];
and I'm thinking I could just add public $build_name; to the Building model, but then I should also add a method to set and get the $build_name. So my Building Model will now look like..
class Building extends Model
{
public $build_name;
protected $fillable =[
'buildingtype_id',
'build_name',
];
public function getBuildName () {
return $this->build_name;
}
public function setBuildName ($name) {
$this->build_name = $name;
}
And I can just create the object myself in the controller...
If I do this, is {!! $value->build_name !!} still appropiate for the view? Or should I now be using {!! $value->getBuildName() !!}
Or am I missing a key concept somewhere? I'm still new to Laravel and OOP.
Edit
I just implemented this, and it's not working. If I add the public $build_name attribute to the model, getBuildName does not return anything, however if I remove public $build_name it does... (which would break my attempting to create that object manually)
When you declare public $build_name, this will override (or more precisely, reset) any other field with the same name in the model. So, you'll have to call setBuildName() setter method before you get it.
I just implemented this, and it's not working. If I add the public $build_name attribute to the model, getBuildName does not return anything
That's because you've called the getter method before the setter, so there is nothing (null) set in the public variable $build_name.
Although you haven't quite mentioned why exactly you want to reuse the Eloquent model, but you can achieve your desired purpose with a little tweak on the model's setter methods:
class Building extends Model
{
/* ... */
public function setBuildName ($name) {
$this->build_name = $name;
return $this;
}
}
Notice returning the current object ($this) in case you would want to chain multiple setter methods in one go.
e.g.:
$model->setBuildName('some name')->setBuildHeight(400);
UPDATE
You can use the default eloquent model to serve your purpose (hence, ridding you of making a duplicate class to achieve roughly the same effect).
Now suppose you have your model Building and would like to set it's attributes manually, then, the following operation on the model is still appropriate:
$building = new App\Building();
$building->build_name = 'Some Building Name'; // you can substitute this with your setter method as well
$building->build_height = 110; // assuming you have a column named `build_height` in your model's table
Note that the only difference in what you'd be doing here is:
You DON'T declare public variables at all in the Eloquent model.
You don't call Eloquent's save() method on the model to persist the manually set data (giving it a transient behavior).
The model now is totally eligible to be passed to your view and you can access it's attributes as you would with a regular model:
<!-- in your view -->
{{ $building->build_name }}
{{ $building->build_height }}
As an alternative approach to setting your arbitrary data, you can have a single setter which accepts an array of key value data to be stored in the model (e.g. ['build_name' => 'Building Name', 'build_height' => 110]):
//in class Building
public function setData($data){
$this->build_name = $data['build_name'];
$this->build_height = $data['build_height'];
}

How to access other input attributes in Validator::extend?

As the question title states:
How can you access other input attributes when using Validator::extend?
Upon inspecting Laravel's built-in Validator class, I can see it uses $this->data to access other attributes; however you can't directly use $thisin the closure that Validator::extend requires.
It seems like manually extending the Validator class (through a custom class) is the only option... Am I correct? If so, this seems to me like a serious limitation for converting validators into packages as each package would extend the base Validator class for which PHP would eventually just retains the last defined extension (and thus rendering other validator packages unusable...). Or am I missing something?
Thanks.
EDIT
I also tried to wrap it up in a package following this method by Jason Lewis but I keep getting a BadMethodCallException stating that the validation method could not be found... The package is psr-0 compliant and I'm pretty sure it's not a namespacing issue.
After a bit of testing, you can access the array if you use a class and not a callback. As it extends the Validator class.
class TestRulesValidator extends \Illuminate\Validation\Validator
{
public function validateTestRule($attribute, $value, $parameters)
{
var_dump($this->data);
exit();
}
}
From the validation documentation, use:
Validator::resolver(function($translator, $data, $rules, $messages) {
return new TestRulesValidator($translator, $data, $rules, $messages);
});
Your rule name would be test_rule. Remove the validate keyword and convert to underscore case.
Just tested this on fresh installation and it works.
Edit - You can also use the normal extend method and pass an extra parameter.
class TestRulesValidator
{
public function validateTestRule($attribute, $value, $params, $validator) {
var_dump($validator->getData());
}
}
Validator::extend('test_rule', 'TestRulesValidator#validateTestRule');

Categories