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

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');

Related

How can I access Laravel 5.1 validation rules

How can I access all the methods that Laravel 5.1 provides us for validation. For example I have made custom request with artisan command php artisan make:request EventRequest in that file there is a public function rules(){ return[]; } in that function yo can specify html attributes names and the validation rules that you need. How can I access these validation rules(path to these rules). Please note that I don't want to make custom validation rules I have to access existing ones.
I'm sure you're already aware of the documented list of available validation rules.
If you just want to access the code that is used to evaluate those rules: in Laravel 5.1, these built-in rule names are mapped to methods defined directly on the Validator class. (You can also check the API reference for that class)
For example, 'digits_between' will eventually use the validateDigitsBetween() method on that class. However, since those are protected methods, you can't call them directly yourself. You have to use Validator::make($request, $rules). See the docs on this.
(In Laravel 5.6, these methods are on a trait called ValidatesAttributes. So if for whatever reason you wanted to use them directly, you could just use that trait on your class.)
In your controller replace the Request with your validation namespace probably like this App\Http\Requests\EventRequest so it should look like this.
from
public function store(Request $requests)
{
// code here
}
to
public function store(App\Http\Requests\EventRequest $requests)
{
// code here
}
or else you can use your validation namespace like so
use App\Http\Requests\EventRequest;
SomeControllerClass extends Controller {
public function store(EventRequest $requests)
{
// code here
}
}
Hope that helps.

Same Laravel resource controller for multiple routes

I am trying to use a trait as a typehint for my Laravel resource controllers.
The controller method:
public function store(CreateCommentRequest $request, Commentable $commentable)
In which the Commentable is the trait typehint which my Eloquent models use.
The Commentable trait looks like this:
namespace App\Models\Morphs;
use App\Comment;
trait Commentable
{
/**
* Get the model's comments.
*
* #return \Illuminate\Database\Eloquent\Relations\MorphMany
*/
public function Comments()
{
return $this->morphMany(Comment::class, 'commentable')->orderBy('created_at', 'DESC');
}
}
In my routing, I have:
Route::resource('order.comment', 'CommentController')
Route::resource('fulfillments.comment', 'CommentController')
Both orders and fulfillments can have comments and so they use the same controller since the code would be the same.
However, when I post to order/{order}/comment, I get the following error:
Illuminate\Contracts\Container\BindingResolutionException
Target [App\Models\Morphs\Commentable] is not instantiable.
Is this possible at all?
So you want to avoid duplicate code for both order and fulfillment resource controllers and be a bit DRY. Good.
Traits cannot be typehinted
As Matthew stated, you can't typehint traits and that's the reason you're getting the binding resolution error. Other than that, even if it was typehintable, the container would be confused which model it should instantiate as there are two Commentable models available. But, we'll get to it later.
Interfaces alongside traits
It's often a good practice to have an interface to accompany a trait. Besides the fact that interfaces can be typehinted, you're adhering to the Interface Segregation principle which, "if needed", is a good practice.
interface Commentable
{
public function comments();
}
class Order extends Model implements Commentable
{
use Commentable;
// ...
}
Now that it's typehintable. Let's get to the container confusion issue.
Contexual binding
Laravel's container supports contextual binding. That's the ability to explicitly tell it when and how to resolve an abstract to a concrete.
The only distinguishing factor you got for your controllers, is the route. We need to build upon that. Something along the lines of:
# AppServiceProvider::register()
$this->app
->when(CommentController::class)
->needs(Commentable::class)
->give(function ($container, $params) {
// Since you're probably utilizing Laravel's route model binding,
// we need to resolve the model associated with the passed ID using
// the `findOrFail`, instead of just newing up an empty instance.
// Assuming this route pattern: "order|fullfilment/{id}/comment/{id}"
$id = (int) $this->app->request->segment(2);
return $this->app->request->segment(1) === 'order'
? Order::findOrFail($id)
: Fulfillment::findOrFail($id);
});
You're basically telling the container when the CommentController requires a Commentable instance, first check out the route and then instantiate the correct commentable model.
Non-contextual binding will do as well:
# AppServiceProvider::register()
$this->app->bind(Commentable::class, function ($container, $params) {
$id = (int) $this->app->request->segment(2);
return $this->app->request->segment(1) === 'order'
? Order::findOrFail($id)
: Fulfillment::findOrFail($id);
});
Wrong tool
We've just eliminated duplicate controller code by introducing unnecessary complexity which is as worse as that. ๐Ÿ‘
Even though it works, it's complex, not maintainable, non-generic and worst of all, dependent to the URL. It's using the wrong tool for the job and is plain wrong.
Inheritance
The right tool to eliminate these kinda problems is simply inheritance. Introduce an abstract base comment controller class and extend two shallow ones from it.
# App\Http\Controllers\CommentController
abstract class CommentController extends Controller
{
public function store(CreateCommentRequest $request, Commentable $commentable) {
// ...
}
// All other common methods here...
}
# App\Http\Controllers\OrderCommentController
class OrderCommentController extends CommentController
{
public function store(CreateCommentRequest $request, Order $commentable) {
return parent::store($commentable);
}
}
# App\Http\Controllers\FulfillmentCommentController
class FulfillmentCommentController extends CommentController
{
public function store(CreateCommentRequest $request, Fulfillment $commentable) {
return parent::store($commentable);
}
}
# Routes
Route::resource('order.comment', 'OrderCommentController');
Route::resource('fulfillments.comment', 'FulfillCommentController');
Simple, flexible and maintainable.
Arrrgh, wrong language
Not so fast:
Declaration of OrderCommentController::store(CreateCommentRequest $request, Order $commentable) should be compatible with CommentController::store(CreateCommentRequest $request, Commentable $commentable).
Even though overriding method parameters works in the constructors just fine, it simply does not for other methods! Constructors are special cases.
We could just drop the typehints in both parent and child classes and go on with our lives with plain IDs. But in that case, as Laravel's implicit model binding only works with typehints, there won't be any automatic model loading for our controllers.
Ok, maybe in a better world.
๐ŸŽ‰Update: See PHP 7.4's support for type variance ๐ŸŽ‰
Explicit route model binding
So what we gonna do?
If we explicitly tell the router how to load our Commentable models, we can just use the lone CommentController class. Laravel's explicit model binding works by mapping route placeholders (e.g. {order}) to model classes or custom resolution logics. So, while we're using our single CommentController we can utilize separate models or resolution logics for orders and fulfillments based on their route placeholders. So, we drop the typehint and rely on the placeholder.
For resource controllers, the placeholder name depends on the first parameter you pass to the Route::resource method. Just do a artisan route:list to find out.
Ok, let's do it:
# App\Providers\RouteServiceProvider::boot()
public function boot()
{
// Map `{order}` route placeholder to the \App\Order model
$this->app->router->model('order', \App\Order::class);
// Map `{fulfillment}` to the \App\Fulfilment model
$this->app->router->model('fulfillment', \App\Fulfilment::class);
parent::boot();
}
Your controller code would be:
# App\Http\Controllers\CommentController
class CommentController extends Controller
{
// Note that we have dropped the typehint here:
public function store(CreateCommentRequest $request, $commentable) {
// $commentable is either an \App\Order or a \App\Fulfillment
}
// Drop the typehint from other methods as well.
}
And the route definitions remain the same.
It's better than the first solution, as it does not rely on the URL segments which are prone to change contrary to the route placeholders which rarely change. It's also generic as all {order}s will be resolved to \App\Order model and all {fulfillment}s to the App\Fulfillment.
We could alter the first solution to utilize route parameters instead of URL segments. But there's no reason to do it manually when Laravel has provided it to us.
Yeah, I know, I don't feel good, too.
You can't typehint traits.
However, you can typehint interfaces. So you can create an interface that requires the methods from the trait and resolve that. Then have your classes implement that interface and you should be OK.
EDIT: As #Stefan has kindly pointed out, it's still likely to be difficult to resolve the interface to a concrete class because it will need to resolve to different classes under different circumstances. You could access the request in the service provider and use the path to determine how to resolve it, but I'm a bit dubious of that. I think putting them in separate controllers and using inheritance/traits to share common functionality may be a better bet, since the methods in each controller can type hint the required object, and then pass them to the equivalent parent method.
For my case I have following resources:
Route::resource('books/storybooks', 'BookController');
Route::resource('books/magazines', 'BookController');
After php artisan route:cache and it creates the route to tie up with 'magazine' model.
The solution is to add following line in app/Providers/RouteServiceProvider.php > boot() method, after parent::boot():
Route::model('magazine', \App\Book::class);
Pay attention to the singular and plural.

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.

Passing in multiple attributes when using Stand alone validators in Yii2

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.

Symfony2 - Call EmailValidator inside a Custom Validator

I'm creating a custom validator constraint to validate a "Contact", which is something like "John Doe <jdoe#example.com>". Following the Cookbook I've created the Constraint Class:
<?php
namespace MyCompany\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class Contact extends Constraint
{
public $message = 'The string "%string%" is not a valid Contact.';
}
and also created the validator:
<?php
namespace MyCompany\MyBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\EmailValidator;
class ContactValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (!preg_match('#(.*)\s+<(.*)>#', $value, $matches)) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
}
$emailValidator = new EmailValidator();
if (isset($matches[2]) && $emailValidator->validate($matches[2], new Email())) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
}
}
}
The point is that I'm trying to use the Symfony's EmailValidator inside my custom validator to check the email is valid. I don't want to reinvent the wheel and validate the email using my own regex.
Everything is ok when trying to validate a valid contact but, testing a contact with invalid email ("Gabriel Garcia <infoinv4l1d3mai1.com>") it craches with a PHP Fatal error:
Fatal error: Call to a member function addViolation() on a non-object in /home/dev/myproject/vendor/symfony/symfony/src/Symfony/Component/Validator/Constraints/EmailValidator.php on line 58
Digging into the EmailValidator.php class, I've realized that the issue is related to the $context (ExecutionContext). Here is line 58 of EmailValidator.php:
$this->context->addViolation($constraint->message, array('{{ value }}' => $value));
Seems that context attribute of the class is null. Anyone knows why? I need to inject it somewhere?
Thanks in advance.
P.S.: I'm using Symfony 2.3. Do not pay attention to the regex, I know it can be so much better. It's just for testing right now.
I think the original question was about using EmailValidator inside a Custom Validator and in this scenario container is unavailable, so
$this->get('validator');
will not work. It seems that the only issue the poster had is to have EmailValidator addViolation to the correct context. This should work:
$emailValidator = new EmailValidator();
$emailValidator->initialize($this->context);
$emailValidator->validate($matches[2], $constraint);
You can directly use the constraint
see http://symfony.com/doc/current/book/validation.html
use Symfony\Component\Validator\Constraints\Email
$emailConstraint = new Email();
// use the validator to validate the value
$errorList = $this->get('validator')->validateValue(
$email,
$emailConstraint
);
Best regard
Finding this topic after have try to call custom inside custom, I made a deeply research and I may just found another better way (simpler according to me).
Valid : Sf2.6>=
$this->context->getValidator()
->inContext($this->context)
->atPath("time_on_point")
->validate($timeOnPoint, new AssertCustom\DateTimeRange(array('min' => '+1 day')));
In this case, I declared a new custom validator like class specific Validator and I could go directly to the field by its name.
The advantage of this : I can call another custom by only applying the "new AssertCustom" and if this "AssertCustom" needs a service like a construct, I won't have a dependency because the configuration service will call all the stuff transparently.
Be careful, if you call recursively (deep) field, you will need to adapt the context according to the comments found in this file : Symfony\Component\Validator\Constraints\CollectionValidator

Categories