I have a app using Laravel framework and there are some conditional rules that I dont know what is the best way to code and maintain.
Use case: conditionally apply promotion code
promo code can be applied within specific date or date range
promo code can be applied on order >= $100
promo code can be applied for specific item
...
Basic solution is to write multiple IF ELSE statements to check 1 by 1. For example:
if ($promo->specific_date) {
}
elseif ($promo->date_range >= 'date' && $promo->specific_date <= 'date') {
}
if ($totalAmount < 100) {
// Dont allow
}
if (! $promo->allowed_items) {
// Dont allow
}
// More conditions ...
I can see that code will be problematic in testing and maintaining.
So Im wondering if there is a better way to handle this? E.g. using a OOP way?
P/S: To clarify my use case:
I need all rules to pass to make a promo valid
Im thinking of creating a Rule model so that I can have a CRUD to manage them, and in the backend, I can run a query to get all rules, then call a class to pipe and check each rules... (not sure if this is good or bad idea)
Thanks,
use switch statement here because it faster then if
by default set promo_code flag false becuase now only want to my any condition it true then set promo_code flag true ..
if you want to used if statement in one by one then it's good because easy to readable and maintainable
$promo_code_flag = false;
if ($promo->specific_date) {
$promo_code_flag = true;
}
elseif ($promo->date_range >= 'date' && $promo->specific_date <= 'date') {
$promo_code_flag = true;
}
if ($totalAmount > 100) {
$promo_code_flag = true;
}
if ( $promo->allowed_items) {
$promo_code_flag = true;
}
if($promo_code_flag) {
//allow promo code
}//other wise it will not allowe
You could define the promotion properties in your Promotion model (which means they should probably be stored somewhere in your database), and then have a normalized validator that you can call for any promo.
Here's some sample/pseudo code to explain the process (it's PHP 7.4, simply remove the property types to make it work for previous versions):
final class Promotion
{
private DateTime $minDate;
private DateTime $maxDate;
private DateTime $minAmount;
private array $allowedItems;
public function getMinDate(): DateTime
{
return $this->minDate;
}
public function getMaxDate(): DateTime
{
return $this->maxDate;
}
public function getMinAmount(): DateTime
{
return $this->minAmount;
}
public function getAllowedItems(): array
{
return $this->allowedItems;
}
}
final class PromotionValidator
{
public function isPromotionValid(Promotion $promo, array $purchasedItems, int $totalAmount): bool
{
$now = new \DateTime();
if ($now < $promo->getMinDate() || $now > $promo->getMaxDate()) {
return false;
}
if ($totalAmount < $promo->getMinAmount()) {
return false;
}
if (count(array_intersect($purchasedItems, $promo->getAllowedItems())) !== count($promo->getAllowedItems())) {
return false;
}
return true;
}
}
Usage:
$promotionValidator = new PromotionValidator();
$promoIsValid = $promotionValidator->isPromotionValid($promo, $cartItems, $cartAmount);
You could leverage Laravel pipelines to apply some kind of checks on your order.
Imagine that you could pluck the constraints configuration correctly from the database and build up an array (or something like a ConstraintBag instance) which contains all the constraints that you need to check:
configuration
$constraints = [
DateRangeConstraint::class,
TotalAmountConstraint::class,
AllowedItemsContraint::class,
];
Each constraint may adhere to the same interface (Constraint in this PoC) which will define a single handle method:
use Closure;
class DateRangeConstraint implements Constraint
{
public function handle($order, Closure $next)
{
if ($order->promo->start_date >= 'date' || $order->promo->end_date <= 'date') {
throw new PromotionConstraintException($this);
}
return $next($order);
}
}
Then in your controller/service method you could use this array of rules in a pipeline and pass the order object (or an object which contains all the parts you need for validating all the constraints) though the constraints. If any of this fails, you could trigger a custom exception (maybe one per category of constraint/one per constraint) and return the resulting outcome of the validation process:
// Do not forget to add the use statement
use Illuminate\Pipeline\Pipeline;
class PromotionValidationService
{
protected $constraints;
// Pass in the constraints array you have already built
public function __construct($constraints)
{
$this->constraints = $constraints;
}
// Start the validation process and cycle through all the constraints
// I would pass in the order object as you might need to access the total
// order amount and/or the items in the order
public function validate($order)
{
try {
app(Pipeline::class)
->send($order)
->through($this->constraints);
} catch (PromotionConstraintException $exception) {
// Handle the exception and return false or rethrow
// the exception for further handling from the caller.
return false;
}
return true;
}
}
Obviously, this is still a proof of concepts and would require more study and architectural planning to handle the various constraints you might need to check (eg: passing the whole $order object might not be the best idea, or it might not be available yet when checking the promotion constraints). However, this could be a flexible alternative to a fixed sequence of if/elses that needs to be edited for each change.
Related
I am trying to run some dynamic method calls based on the value of a database field. Some context: I have a model Anniversary and I want to display all upcoming anniversaries within the next x days. An anniversary has a date and a frequency. For example, monthly, quarterly, etc. Based on the frequency, I want to check for each anniversary if it is upcoming.
Here is my code so far:
$anniversaries = auth()->user()->anniversaries()->get();
$test = $anniversaries->filter(function ($anniversary) {
$method = Str::of($anniversary->frequency)->camel();
return ${$anniversary->$method}() == true;
});
dd($test);
The above works, when in the actual method I dd() something. But when returning true or false, I get the error:
App\Models\Anniversary::monthly must return a relationship instance
And in my model I just have a few methods like below, for testing:
public function monthly()
{
return true;
}
public function quarterly()
{
return false;
}
My only question is, I want to understand why I am getting this error and ofcourse any pointers in the right direction to get what I want to work. Thanks!
The following line creates an Illuminate\Support\Str object instead of a string. This causes the Method name must be a string error.
$method = Str::of($anniversary->frequency)->camel();
You can fix this by manually casting it to a string and invoking it directly:
$test = $anniversaries->filter(function ($anniversary) {
$method = (string) (Str::of($anniversary->frequency)->camel());
return $anniversary->$method() == true;
});
Throwing in my 2 cents for this as well. The Str::of(), which are "Fluent Strings" added in Laravel 7.x return an instance of Stringable:
https://laravel.com/api/8.x/Illuminate/Support/Stringable.html
For example:
dd(Str::of('monthly')->camel());
Illuminate\Support\Stringable {#3444
value: "monthly"
}
To get the value of this, as a string and not an object, you can cast it (as shown in MaartenDev's answer), or call the __toString() method:
dd(Str::of('monthly')->camel()->__toString());
"monthly"
In your code example, that would simply be:
$method = Str::of($anniversary->frequency)->camel()->__toString();
return $anniversary->{$method}() == true;
Alternatively, you can just use the Str::camel() function to bypass this Stringable class:
$method = Str::camel($anniversary->frequency);
return $anniversary->{$method}() == true;
https://laravel.com/docs/8.x/helpers#method-camel-case
Hope that helps clear up some confusion 😄
you have issue in this part ${$anniversary->$method}(). if you access a function like property laravel models thinks its relation function.
so replace with $anniversary->{$method}()
try this one
$anniversaries = auth()->user()->anniversaries()->get();
$test = $anniversaries->filter(function ($anniversary) {
$method = Str::of($anniversary->frequency)->camel();
return $anniversary->{$method}() == true;
});
dd($test);
How to stop one validation rule if other rule is not succeeded in laravel
I have an input type for date and I have 3 validation rule in there which are:
date
date_format:m/d/Y
after:date('m/d/Y).
When I enter an invalid data like 04/05/2014dsada the after date rule is still running. How can I stop running the after validation rule?
How to finish this in Laravel??
Here's my sample code:
$rules = array('mmad_starting_date' => 'date|required|date_format:m/d/Y|after:'.date("m/d/Y H:i"));
if i will use laravel Conditionally Adding Rules.
$validator->sometimes('mmad_starting_date', 'after:'.date(m/d/Y), function($input){
//how can i check if the mad_starting_date is a valid date
return $input->mmad_starting_date = (what should i input here);
});
You can avoid passing on to next rule for an attribute if earlier one has already failed.
Just extend the Illuminate\Validation\Validator class and override the passes method.
Break the loop as soon as you encounter error in MessageBag for that attribute.
public function passes()
{
$this->messages = new MessageBag;
// We'll spin through each rule, validating the attributes attached to that
// rule. Any error messages will be added to the containers with each of
// the other error messages, returning true if we don't have messages.
foreach ($this->rules as $attribute => $rules)
{
foreach ($rules as $rule)
{
$this->validate($attribute, $rule);
/* If the error MessageBag has error for the $attribute, break */
if($this->messages->has($attribute))
break;
}
}
// Here we will spin through all of the "after" hooks on this validator and
// fire them off. This gives the callbacks a chance to perform all kinds
// of other validation that needs to get wrapped up in this operation.
foreach ($this->after as $after)
{
call_user_func($after);
}
return count($this->messages->all()) === 0;
}
In Laravel 5.2 you can do that easily by adding bail rule. If you add bail rule to an attribute, validation will not check other rules if one rule fail.
In your case you can simply use,
$rules = array('mmad_starting_date' => 'bail|required|date_format:m/d/Y|after:'.date("m/d/Y H:i"));
Refer Laravel validation doc here.
You could try this: (basically the content of the date_format rule...)
$validator->sometimes('mmad_starting_date', 'after:'.date(m/d/Y), function($input){
$parsed = date_parse_from_format('m/d/Y', $input->mmad_starting_date);
return $parsed['error_count'] === 0 && $parsed['warning_count'] === 0;
});
Thnx lukasgeiter your solutions worked,
what i did also is create a custom validator in my corevalidator class and create two functions and it works.
public function validateAfterDatetoday($attribute, $value, $parameters){
$today = new DateTime(date("m/d/Y H:i"));
if($this->validateThisDate($value)){
$date = new DateTime($value);
if($today > $date){
return false;
}
}
return true;
}
function validateThisDate($date){
$d = DateTime::createFromFormat('m/d/Y', $date);
if($d){
return true;
}
return false;
}
and use it in my mmad_starting_date field. anyways your solution is better.
In PHP I need to pass some arguments to a function by reference.
I don't want to write 2 different methods for similar behaviour.
So i need to select behaviour by argument.
But I can't pass null by reference.
So I created a dummy array.
So i run it either by
$temp[0]=-1;
$this->doSomething($bigIds, $temp);
or
$temp[0]=-1;
$this->doSomething($temp, $smallIds);
public function doSomething(&$bigIds, &$smallIds) {
if ($bigIds[0] != -1) {
// make some thing
}
if ($smallIds[0] != -1) {
// make some thing
}
}
Is there a better/ elegant way to do this?
I would suggest an enum but this is PHP. So this should do it for you:
class YourClass
{
const DoSomethingSmall = 0;
const DoSomethingBig = 1;
public function doSomething(&$ids, $actionType) {
// can also use a switch here
if($actionType == self::DoSomethingSmall) {
// do small
}
else if($actionType == self::DoSomethingBig) {
// do big
}
}
}
Then you can do:
$this->doSomething($bigIds, self::DoSomethingBig);
$this->doSomething($smallIds, self::DoSomethingSmall);
From outside the class you can use YourClass::DoSomethingBig and YourClass::DoSomethingSmall
There could be loads of things you might rather do, for instance what #ad7six says in a comment, and you could also just give it some sort of setting and just one array..
public function doSomething(&$bIds, $mode) {
switch($mode){
case 1: do smallthing;break;
case 2: do bigthing;break;
case 3: do both;break;
default: do nothing;break;
}
It all depends on what you need really
I have lots of code like this in my constructors:-
function __construct($params) {
$this->property = isset($params['property']) ? $params['property'] : default_val;
}
Some default values are taken from other properties, which was why I was doing this in the constructor. But I guess it could be done in a setter instead.
What are the pros and cons of this method and is there a better one?
Edit: I have some dependencies where if a property is not supplied in the $params array then the value is taken from another property, however that other property may be optional and have a default value, so the order in which properties are initialized matters.
This means that if I used getters and setters then it is not obvious which order to call them in because the dependencies are abstracted away in the getter instead of being in the constructer...
I would suggest you, to write proper getter/setter functions, which assert you the correct data-type and validations (and contain your mentioned default-value logic). Those should be used inside your constructor.
When setting multiple fields, which depend on each other, it seems to be nice to have a separate setter for this complex data. In which kind of way are they depending anyway?
e.g.:
// META-Config
protected $static_default_values = array(
"price" => 0.0,
"title" => "foobar"
// and so on
);
protected $fallback_getter = array(
"price" => "getfallback_price"
);
// Class Logic
public function __construct($params){
$this->set_properties($params);
}
public set_properties($properties){
// determines the sequence of the setter-calls
$high_prio_fields = array("price", "title", "unimportant_field");
foreach($high_prio_fields as $field){
$this->generic_set($field, $properties[$field]);
// important: unset fields in properties-param to avoid multiple calls
unset($properties[$field]);
}
foreach($properties as $field => $value){
$this->generic_set($field, $value);
}
}
// this could also be defined within the magic-setter,
// but be aware, that magic-functions can't be resolved by your IDE completely
// for code-completion!
private function generic_set($field, $value){
// check if setter exists for given field-key
$setter_func = "set_".$v;
if(method_exists($this, $setter_func){
call_user_func_array(array($this, $setter_func), array($v));
}
// else => just discard :)
}
// same comment as generic-set
private function generic_get($field){
// check if value is present in properties array
if(isset($this->properties[$field]){
return $this->properties[$field];
}
// check if fallback_getter is present
if(isset($this->fallback_getter[$field]){
return call_user_func_array(array($this, $this->fallback_getter[$field]));
}
// check for default-value in meta-config
if(isset($this->static_default_values[$field]){
return $this->static_default_values[$field];
}
// else => fail (throw exception or return NULL)
return null;
}
public function get_price(){
// custom getter, which ovverrides generic get (if you want to)
// custom code...
return $this->generic_get("price");
}
private function getfallback_price(){
return $this->properties["other_value"] * $this->properties["and_another_value"];
}
public function set_price($price){
$price = (float) $price; // convert to correct data-type
if($price >= 0.0){
$this->properties["price"] = $price;
}
// else discard setting-func, because given parameter seems to be invalid
// optional: throw exception or return FALSE on fail (so you can handle this on your own later)
}
Update to your edit:
the modified source-code should solve all your demands (order of setter-funcs, different resolvings of get-value).
Create "globally available" function array_get.
public static function array_get($array, $property, $default_value = null) {
return isset($array[$property]) ? $array[$property] : $default_value;
}
When having a lot of default options and you need to be able to overwrite them - as you have maybe seen in jQuery using .extend() before - I like to use this simple and quick method:
class Foo {
private $options;
public function __construct($override = array()) {
$defaults = array(
'param1' => 'foo',
'param2' => ...,
'paramN' => 'someOtherDefaultValue');
$this->options= array_replace_recursive($defaults, $override);
}
}
Especially for getting classes started this is a very easy and flexible way, but as already has been mentioned if that code is going to be heavily used then it probably not a bad idea to introduce some more control over those options with getters and setters, especially if you need to take actions when some of those options are get or set, like in your case dependencies if I understood your problem correctly.
Also note that you don't have to implement getters and setters yourself, in PHP you can use the __get and __set magic methods.
It follows some useless code that hopefully gives some ideas:
[...inside Foo...]
public function __set($key, $value){
switch(true){
//option exists in this class
case isset($this->options[$key]):
//below check if $value is callable
//and use those functions as "setter" handlers
//they could resolve dependencies for example
$this->options[$key] = is_callable($value) ? $value($key) : $value;
break;
//Adds a virtual setter to Foo. This so called 'magic' __set method is also called if the property doesn't exist in the class, so you can add arbitrary things.
case $key === 'someVirtualSetterProp': Xyzzy::Noop($value); break;
default:
try{ parent::__set($key, $value); } catch(Exception $e){ /* Oops, fix it! */ }
}
}
Note that in the above examples I squeezed in different approaches and it usually doesn't make sense to mix them like that. I did this only to illustrate some ideas and hopefully you will be able to decide better what suits your needs.
So, I have a object with structure similar to below, all of which are returned to me as stdClass objects
$person->contact->phone;
$person->contact->email;
$person->contact->address->line_1;
$person->contact->address->line_2;
$person->dob->day;
$person->dob->month;
$person->dob->year;
$album->name;
$album->image->height;
$album->image->width;
$album->artist->name;
$album->artist->id;
etc... (note these examples are not linked together).
Is it possible to use variable variables to call contact->phone as a direct property of $person?
For example:
$property = 'contact->phone';
echo $person->$property;
This will not work as is and throws a E_NOTICE so I am trying to work out an alternative method to achieve this.
Any ideas?
In response to answers relating to proxy methods:
And I would except this object is from a library and am using it to populate a new object with an array map as follows:
array(
'contactPhone' => 'contact->phone',
'contactEmail' => 'contact->email'
);
and then foreaching through the map to populate the new object. I guess I could envole the mapper instead...
If i was you I would create a simple method ->property(); that returns $this->contact->phone
Is it possible to use variable variables to call contact->phone as a direct property of $person?
It's not possible to use expressions as variable variable names.
But you can always cheat:
class xyz {
function __get($name) {
if (strpos($name, "->")) {
foreach (explode("->", $name) as $name) {
$var = isset($var) ? $var->$name : $this->$name;
}
return $var;
}
else return $this->$name;
}
}
try this code
$property = $contact->phone;
echo $person->$property;
I think this is a bad thing to to as it leads to unreadable code is is plain wrong on other levels too, but in general if you need to include variables in the object syntax you should wrap it in braces so that it gets parsed first.
For example:
$property = 'contact->phone';
echo $person->{$property};
The same applies if you need to access an object that has disalowed characters in the name which can happen with SimpleXML objects regularly.
$xml->{a-disallowed-field}
If it is legal it does not mean it is also moral. And this is the main issue with PHP, yes, you can do almost whatever you can think of, but that does not make it right. Take a look at the law of demeter:
Law of Demeter
try this if you really really want to:
json_decode(json_encode($person),true);
you will be able to parse it as an array not an object but it does your job for the getting not for the setting.
EDIT:
class Adapter {
public static function adapt($data,$type) {
$vars = get_class_vars($type);
if(class_exists($type)) {
$adaptedData = new $type();
} else {
print_R($data);
throw new Exception("Class ".$type." does not exist for data ".$data);
}
$vars = array_keys($vars);
foreach($vars as $v) {
if($v) {
if(is_object($data->$v)) {
// I store the $type inside the object
$adaptedData->$v = Adapter::adapt($data->$v,$data->$v->type);
} else {
$adaptedData->$v = $data->$v;
}
}
}
return $adaptedData;
}
}
OOP is much about shielding the object's internals from the outside world. What you try to do here is provide a way to publicize the innards of the phone through the person interface. That's not nice.
If you want a convenient way to get "all" the properties, you may want to write an explicit set of convenience functions for that, maybe wrapped in another class if you like. That way you can evolve the supported utilities without having to touch (and possibly break) the core data structures:
class conv {
static function phone( $person ) {
return $person->contact->phone;
}
}
// imagine getting a Person from db
$person = getpersonfromDB();
print conv::phone( $p );
If ever you need a more specialized function, you add it to the utilities. This is imho the nices solution: separate the convenience from the core to decrease complexity, and increase maintainability/understandability.
Another way is to 'extend' the Person class with conveniences, built around the core class' innards:
class ConvPerson extends Person {
function __construct( $person ) {
Person::__construct( $person->contact, $person->name, ... );
}
function phone() { return $this->contact->phone; }
}
// imagine getting a Person from db
$person = getpersonfromDB();
$p=new ConvPerson( $person );
print $p->phone();
You could use type casting to change the object to an array.
$person = (array) $person;
echo $person['contact']['phone'];
In most cases where you have nested internal objects, it might be a good time to re-evaluate your data structures.
In the example above, person has contact and dob. The contact also contains address. Trying to access the data from the uppermost level is not uncommon when writing complex database applications. However, you might find your the best solution to this is to consolidate data up into the person class instead of trying to essentially "mine" into the internal objects.
As much as I hate saying it, you could do an eval :
foreach ($properties as $property) {
echo eval("return \$person->$property;");
}
Besides making function getPhone(){return $this->contact->phone;} you could make a magic method that would look through internal objects for requested field. Do remember that magic methods are somewhat slow though.
class Person {
private $fields = array();
//...
public function __get($name) {
if (empty($this->fields)) {
$this->fields = get_class_vars(__CLASS__);
}
//Cycle through properties and see if one of them contains requested field:
foreach ($this->fields as $propName => $default) {
if (is_object($this->$propName) && isset($this->$propName->$name)) {
return $this->$propName->$name;
}
}
return NULL;
//Or any other error handling
}
}
I have decided to scrap this whole approach and go with a more long-winded but cleaner and most probably more efficient. I wasn't too keen on this idea in the first place, and the majority has spoken on here to make my mind up for me. Thank for you for your answers.
Edit:
If you are interested:
public function __construct($data)
{
$this->_raw = $data;
}
public function getContactPhone()
{
return $this->contact->phone;
}
public function __get($name)
{
if (isset($this->$name)) {
return $this->$name;
}
if (isset($this->_raw->$name)) {
return $this->_raw->$name;
}
return null;
}
In case you use your object in a struct-like way, you can model a 'path' to the requested node explicitly. You can then 'decorate' your objects with the same retrieval code.
An example of 'retrieval only' decoration code:
function retrieve( $obj, $path ) {
$element=$obj;
foreach( $path as $step ) {
$element=$element[$step];
}
return $element;
}
function decorate( $decos, &$object ) {
foreach( $decos as $name=>$path ) {
$object[$name]=retrieve($object,$path);
}
}
$o=array(
"id"=>array("name"=>"Ben","surname"=>"Taylor"),
"contact"=>array( "phone"=>"0101010" )
);
$decorations=array(
"phone"=>array("contact","phone"),
"name"=>array("id","name")
);
// this is where the action is
decorate( $decorations, &$o);
print $o->name;
print $o->phone;
(find it on codepad)
If you know the two function's names, could you do this? (not tested)
$a = [
'contactPhone' => 'contact->phone',
'contactEmail' => 'contact->email'
];
foreach ($a as $name => $chain) {
$std = new stdClass();
list($f1, $f2) = explode('->', $chain);
echo $std->{$f1}()->{$f2}(); // This works
}
If it's not always two functions, you could hack it more to make it work. Point is, you can call chained functions using variable variables, as long as you use the bracket format.
Simplest and cleanest way I know of.
function getValueByPath($obj,$path) {
return eval('return $obj->'.$path.';');
}
Usage
echo getValueByPath($person,'contact->email');
// Returns the value of that object path