Model event is not triggered on laravel - laravel

I have a class that inherits a base class and uses a trait ... I will put the code below ..
The base class is using basically to do a validation before the rescue, using for this the saving event in the boot.
The trait is to tell the class to use uuid in the id attribute .. this trait uses the creating event of the boot.
In the class itself, the boot saving event is used to check if an active record exists.
In this code the trait creating event is not being triggered ... I can not do a save because uuid is not generated ... if I take the boot method in the final class the creating event is executed ...
something I'm not seeing ... does anybody have any idea what may be happening?
MAIN CLASS
class AcademicYear extends BaseModel
{
use UseUuid;
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::saving(function($model)
{
if($model->attributes['disable'] == false){
$model->searchActiveRecord();
}
});
}
public function searchActiveRecord(){
if ($this::where('disable', false)->count() >= 1){
throw new \App\Exceptions\OperationNotAllowed('operation not allowed', 'there is an active record', '422');
}
return true;
}
}
BASE MODEL
class BaseModel extends Model
{
/**
* If the model will be validated in saving
*
* #var bool
*/
protected static $validate = true;
/**
* Rules that will be used to validate the model
*
* #var array
*/
protected $validationRules = [];
/**
* Create a new base model instance.
*
* #param array $attributes
* #return void
*/
public function __construct($attributes = [])
{
parent::__construct($attributes);
}
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::saving(function($model)
{
if ($model::$validate) {
$model->validate();
}
});
}
/**
* Execute validation of model attributes.
*
* #return void
*/
public function validate()
{
$validator = Validator::make($this->attributesToArray(), $this->validationRules);
if($validator->fails()) {
throw new \App\Exceptions\OperationNotAllowed('validation failed', $validator->messages(), '422');
}
return true;
}
}
TRAIT
trait UseUuid
{
/**
* The "booting" method of the model.
*
* #return void
*/
protected static function boot()
{
parent::boot();
static::creating(function ($model)
{
$model->incrementing = false;
$model->keyType = 'string';
$model->{$model->getKeyName()} = Str::uuid()->toString();
});
static::retrieved(function ($model)
{
$model->incrementing = false;
});
}
}

Your model's boot method is conflicting with the trait's boot method, because they have the same name.
From the PHP.net manual on Traits:
An inherited member from a base class is overridden by a member inserted by a Trait. The precedence order is that members from the current class override Trait methods, which in turn override inherited methods.
Current class: AcademicYear
Trait: UseUuid
Inherited class: BaseModel
If you want to use a boot method on an individual model, you'll have to alias the trait's method to something different:
class AcademicYear extends BaseModel
{
use UseUuid {
boot as uuidBoot;
}
// ...
protected static function boot()
{
static::uuidBoot();
// Your model-specific boot code here.
}
}
Be careful with where you place parent::boot(). If you call parent::boot() in both your trait and your model, BaseModel::boot() will be called more than once.

Related

How to pass arguments to a mocked class constructor

I have one method (getUsers()) in my class that I would like to mock but I have a constructor in my class. How can I pass values to the constructor when mocking my class?
class MyNotifications {
/**
* Date time.
*
* #var DateTime|mixed|null
*/
public $date;
public function __construct($date = NULL)
{
if (!$date) {
$date = new \DateTime();
}
$this->date = $date;
}
/**
* Get users.
*
* #param int $node_id
* Node id.
*
* #return mixed
* #throws \GuzzleHttp\Exception\GuzzleException
*/
public function getUsers($node_id)
{
// API code goes here.
}
/**
* Get day.
*
* #return false|int|string
*/
public function getDay()
{
return $this->date->format('d');
}
}
class MyNotificationsTest extends TestCase
{
use RefreshMigrations;
public function testOneDay()
{
$mock = $this->getMockBuilder(MyNotifications::class)->onlyMethods([
'getUsers',
])->getMock();
$mock->method('getUsers')->willReturn(['User 1']);
dump($mock->getDay());
dump($mock->getUsers(1));
}
}
For example, I would like to pass the date "2021-12-22" to the constructor so the getDay() method returns 22 instead of the current day.
I haven't used PHPUnit mocks before (usually defaulting to Mockery) but looking at the documentation are you able to call setConstructorArgs(array $args) on the getMockBuilder?
class MyNotificationsTest extends TestCase
{
use RefreshMigrations;
public function testOneDay()
{
$mock = $this->getMockBuilder(MyNotifications::class)
->onlyMethods([
'getUsers',
])
->setConstructorArgs(['2021-03-08'])
->getMock();
$mock->method('getUsers')->willReturn(['User 1']);
dump($mock->getDay());
dump($mock->getUsers(1));
}
}

Can not get Request data in custom service?

I have custom service:
<?php
namespace App\Library\Services;
use Illuminate\Http\Request;
class RegisterCustomerService
{
private $request;
public function constructor(Request $request)
{
$this->request = $request;
}
public function register($role)
{
dd($this->request);
}
}
Why I can not get dd($this->request); when I do POST request:
$customer = $registerCustomerService->register('customer');
My service provider is:
class RegisterCustomerServiceProvider extends ServiceProvider
{
/**
* Register services.
*
* #return void
*/
public function register(Request $request)
{
$this->app->bind('App\Library\Services\RegisterCustomerService', function ($app) {
return new RegisterCustomerService($request);
});
}
/**
* Bootstrap services.
*
* #return void
*/
public function boot()
{
//
}
}
You don't need to bind the instance in the container because Laravel can automatically resolve the namespace and the class dependencies if you resolve an object through the container.
You can the remove the binding from the service provider and use:
$customer = app('App\\Library\\Services\\RegisterCustomerService')->register('customer');
In this way the container will resolve the Register customer service and will create that with all the needed dependencies (the request object in your example).

How can I create a controller constructor in Laravel that takes in two concrete implementations of the same interface?

Background
Note: this is using Laravel 5.3, Please don't judge.
We are trying to use dependency injection with our laravel controllers and push as much business logic into repos that are injected to controllers upon the controller instantiation.
We already have this functioning example:
class AcmeController extends Controller
{
protected $repository;
public function __construct(AcmeInterface $repository)
{
$this->repository = $repository;
}
}
inside app/Providers/RepositoryServiceProvider.php we do the binding:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->bind(\App\Repositories\Contracts\AcmeInterface::class, \App\Repositories\OpCity\AcmeRepo::class);
}
}
and then the AcmeRepo naturally implements the AcmeInterface:
class AcmeRepo implements AcmeInterface
Question
right now we have a case where some of the data of the same model is persisted in a memory type storage (redis) and the rest is persisted in relational db storage (psql). We would like to have two separate repos where each repo is specific to its storage type, ie RedisAcmeRepo and SqlAcmeRepo
How is it possible to do this in the AcmeController constructor?
public function __construct(AcmeInterface $sqlRepo, AcmeInterface $redisRepo)
{
$this->sqlRepo = $sqlRepo;
$this->redisRepo = $redisRepo;
}
For example you may do this:
$this->app->bind(AcmeController::class, function ($app) {
return new AcmeController($app->make(sqlRepo::class), $app->make(redisRepo::class));
});
Or this:
$this->app->when(AcmeController::class)
->needs('$sqlRepo')
->give($app->make(sqlRepo::class));
$this->app->when(AcmeController::class)
->needs('$redisRepo')
->give($app->make(redisRepo::class));
based on the answers above I came up with this solution, that kind of uses the composite pattern as well (I changed the name of the repos from Acme to ShopperLogs):
<?php
interface ShopperLogInterface
{
public function getLogs($from, $to, $shopper);
}
class ShopperLogsController extends Controller
{
/**
* service
*
* #var \App\Repositories\Contracts\ShopperLogInterface
* #access protected
*/
protected $manager;
public function __construct(ShopperLogInterface $manager)
{
$this->manager = $manager;
}
}
class ShopperLogManager implements ShopperLogInterface
{
protected $sqlRepo;
protected $redisRepo;
public function __construct(ShopperLogInterface $sqlRepo, ShopperLogInterface $redisRepo)
{
$this->sqlRepo = $sqlRepo;
$this->redisRepo = $redisRepo;
}
public function getLogs($from, $to, $shopper)
{
$todayRange = //get the today part of from -- to
/**
* array of ShopperLogs
*/
$todaysLogs;
if ($todayRange) {
$this->redisRepo->getLogs($todayRange->start, $todayRange->finish, $shopper);
}
$legacyRange = //get the part of from -- to that excludes today's range
/**
* array of ShopperLogs
*/
$legacyLogs;
if ($legacyLogs) {
$this->sqlRepo->getLogs($todayRange->start, $todayRange->finish, $shopper);
}
return merge($todayRange, $legacyRange);
}
}
class ShopperLogsSqlRepo implements ShopperLogInterface
{
/**
* #var /Illuminate\Database\Eloquent\Model/ShopperLogs
*/
protected $model;
/**
* #param /Illuminate\Database\Eloquent\Model/ShopperLogs $model
*/
public function __construct(ShopperLogs $model)
{
$this->model = $model;
}
public function getLogs($from, $to, $shopper)
{
$this->model->whereLogs //do eloquent sql stuff here
}
}
class ShopperLogsRedisRepo implements ShopperLogInterface
{
/**
* #var \Redis\Model\Class
*/
protected $model;
/**
* #param \Redis\Model\Class $model
*/
public function __construct(ShopperLogs $model)
{
$this->model = $model;
}
public function getLogs($from, $to, $shopper)
{
$this->model->whereLogs //do redis stuff
}
}
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class RepositoryServiceProvider extends ServiceProvider
{
/**
* Bootstrap the application services.
*
* #return void
*/
public function boot()
{
//
}
/**
* Register the application services.
*
* #return void
*/
public function register()
{
$this->app->bind(\App\Repositories\Contracts\ShopperLogInterface::class, \App\Managers\ShopperLogManager::class);
$this->app->bind(ShopperLogsController::class, function ($app) {
return new ShopperLogsController($app->make(ShopperLogManager::class));
});
$this->app->bind(\App\Repositories\Contracts\ShopperLogInterface::class, function() {
return new \App\Managers\ShopperLogManager(new \App\Repositories\ShopperLogsSqlRepo(new \App\ShopperLog), new \App\Repositories\ShopperLogsRedisRepo(new \App\ShopperLog));
});
}
}

Can an Eloquent model has multiple Observer?

Hi I want write a trait to add an observer to model but I thought write boot method is not the right way and finnaly i find that i can boot trait like boot[TraitName] but i wonder if i add an observer with code like this:
trait CreateObserver
{
public static function bootCreateObserver()
{
static::creating(function (Model $model) {
// ...
});
}
}
can I add another observer for my model like below or it will overriding my trait observer?
class MyModel extends Model
{
use CreateObserver;
public static function boot()
{
static::creating(function ($model) {
// ...
});
}
...
}
That's not the right way. I think this might help you:
https://laravel.com/docs/5.6/eloquent#observers
You bind observers to your models using a service boot:
<?php
namespace App\Providers;
use App\User;
use App\Observers\UserObserver;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*
* #return void
*/
public function boot()
{
User::observe(UserObserver::class);
}
/**
* Register the service provider.
*
* #return void
*/
public function register()
{
//
}
}
Inside the observer you can add all desired functionality:
<?php
namespace App\Observers;
use App\User;
class UserObserver
{
/**
* Listen to the User created event.
*
* #param \App\User $user
* #return void
*/
public function created(User $user)
{
//
}
/**
* Listen to the User deleting event.
*
* #param \App\User $user
* #return void
*/
public function deleting(User $user)
{
//
}
}
And to elaborate. Yes it can have multiple observers. Although I never seen a useful situation for that:
public function boot()
{
User::observe(UserObserver::class);
User::observe(AuthenticableModelsObserver::class);
}
This way both the UserObserver() and AuthenticableModelsObserver() are binded to the User() model on boot.

How to make text field compulsory only when the checkbox is checked with symfony annotation validations in model classes

It is probably a very simple question but I've run out of juice here. Vat field is compulsory only when isVatable checkbox is check by user otherwise it can be ignored. How do I achieve this with group validation (annotations) in model class, not entity?
I checked Validation Groups and Group Sequence but to be honest didn't get my head around.
FormType
class UserType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options = [])
{
$builder
->setMethod($options['method'])
->setAction($options['action'])
->add('vat', 'text')
->add('isVatable', 'checkbox')
;
}
public function getName()
{
return 'user';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
['data_class' => 'My\FrontendBundle\Model\UserModel']
);
}
}
ModelClass
class UserModel
{
/**
* #Assert\NotBlank(message="Vat is required only when checkbox is checked.")
*/
protected $vat;
/**
* #var bool
*/
protected $isVatable = false;
}
I find that the #Assert\True() constraint on a method usually works well for me for these sorts of validation scenario. You can add some validation constraints to methods as well as properties, which is pretty powerful.
The basic idea is that you can create a method, give it this annotation - if the method returns true the validation passes; if it returns false it fails.
class UserModel
{
/**
* #var string
*/
protected $vat;
/**
* #var bool
*/
protected $isVatable = false;
/**
* #Assert\True(message="Please enter a VAT number")
*/
public function isVatSetWhenIsVatableChecked()
{
// if this property is unchecked we don't
// want to do any validation so return true
if (!$this->isVatable) {
return true;
}
// return true if $this->vat is not null
// you might want to add some additional
// validation here to make sure the
return !is_null($this->vat);
}
}
Additionally, you can map the error message to a specific form field with the error_mapping option in your FormType object, as documented here:
http://symfony.com/blog/form-goodness-in-symfony-2-1#error-mapping-fu
Hope this helps :)
Perhaps something like this in ...\Validator\Constraints:
VAT.php
use Symfony\Component\Validator\Constraint;
class VAT extends Constraint
{
public $message = 'VAT is compulsory for applicable items';
public $vat;
public $isVatable;
}
VATConstraint.php
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\PropertyAccess\PropertyAccess;
class VATValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$accessor = PropertyAccess::createPropertyAccessor();
$data = $accessor->getValue($this->context->getRoot(), 'data');
$vat = $data['vat'];
$isVatable = $data['isVatable'];
if ($isVatable && empty($vat)) {
$this->context->addViolation($constraint->message, array('%string%' => $value));
return false;
}
return true;
}
}
User model Entity:
use YourBundle\Validator\Constraints as MyAssert;
class UserModel
{
/**
* #MyAssert\NotBlank(message="Vat is required only when checkbox is checked.")
*/
protected $vat;
...
}
You should use a Class Constraint Validator
First make your Constraint class and your validatorClass:
<?php
// AppBundle/Validator/Constraints/NotBlankIfTaxEnabled.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* #Annotation
*/
class NotBlankIfTaxEnabled extends Constraint
{
public $message = 'If isVat is enabled you have to enter a value in the Vat field.';
public function getTargets()
{
return self::CLASS_CONSTRAINT;
}
}
and
<?php
// AppBundle/Validator/Constraints/NotBlankIfTaxEnabledValidator.php
namespace AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class NotBlankIfTaxEnabledValidator extends ConstraintValidator
{
public function validate($customer, Constraint $constraint)
{
if($customer->getIsVatable() && strlen($customer->getVat()) == 0)
{
$this->context->buildViolation($constraint->message)
->addViolation();
}
}
}
Then add de classcontraint to your Entity/Model
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Validator\Constraints as AppAssert;
/**
* Customer
*
* #ORM\Table()
* #ORM\Entity
* #AppAssert\NotBlankIfTaxEnabled
*/
class Customer
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=64)
*
*/
private $vat;
/**
* #var boolean
*
* #ORM\Column(name="taxEnabled", type="boolean")
*/
private $isVatable;
and do not forget to switch off the required attribute for both fields in your formType:
<?php
namespace AppBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class CustomerType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('vat', NULL, array('required' => FALSE))
->add('isVatable', NULL, array('required' => FALSE))
;
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Customer'
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_customer';
}
}

Resources