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);
Related
One of the routes I'm making for my API in laravel requires me to pass a variable to a ->each() function.
This can be seen below:
public function by_location($zone_id)
{
$zone = Zone::where('id', $zone_id)->get()[0];
error_log($zone->id);
$exhibitors = Exhibitor::where('zone_id', $zone_id)->get();
$exhibitors->each(function($exhibitor, $zone)
{
error_log($zone->id);
$exhibitor['zone_info'] = $zone;
});
return response()->json($exhibitors);
}
This first error_log outputs '2', but with the second I get 'Trying to get property 'id' of non-object'.
Any help is apprecited!
You probably want to use $zone which you selected from database on first line.
Also if you want to change value of item you are iterating you have to use ->map() instead of ->each()
I changed ->get()[0] to ->first(). Never use ->get()[0]
public function by_location($zone_id)
{
$zone = Zone::where('id', $zone_id)->first();
error_log($zone->id);
$exhibitors = Exhibitor::where('zone_id', $zone_id)->get();
$exhibitors->map(function($exhibitor) use ($zone){
error_log($zone->id);
$exhibitor['zone_info'] = $zone;
return $exhibitor;
});
return response()->json($exhibitors);
}
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.
How may I use this function and inform only the first and the last arguments?
Function
function foo($first = false, $second = false, $third = false, $last = false)
{
if($first && $last)
{
echo 'ok';
}
}
I've tried the code below, but it didn't work...
foo($first = true, $last = true);
PHP doesn't do named arguments as python does. See this question for more info.
However, life can be made easier by using other techniques like...
Modify the signature of the function to accept arguments as an associative array
Function
function foo($parameters)
{
// Provide default values if parameters are not specified
$first = isset($parameters['first']) ? $parameters['first'] : false;
$second = isset($parameters['second']) ? $parameters['second'] : false;
$third = isset($parameters['third']) ? $parameters['third'] : false;
$last = isset($parameters['last']) ? $parameters['last'] : false;
if($first && $last)
{
echo 'ok';
}
}
Call
foo(['first' => true, 'last' => true]);
This way is suitable when you have a number of parameters big and variative enough and you have a complex logic inside the function so that writing all this code pays off.
It is not very convenient, however, because the default values are specified not in an obvious way, there's extra code and it's hard to track parameter usages.
Modify the signature of the function to accept a parameter object which holds all the necessary info
This is the way to go for complex signatures and especially if you have a cascade of methods which use the same arguments. I love it because it solved a big problem with passing up to 10 query parameters through processing pipeline. Now it's just one object with possibility to find every parameter usage and friendly autosuggestion of available parameters when typing ->.
Parameter object class
class ParameterObject
{
public $first = false;
public $second = false;
public $third = false;
public $last = false;
}
Function
function foo(ParameterObject $paramObj)
{
if($paramObj->first && $paramObj->last)
{
echo 'ok';
}
}
Call
$paramObj = new ParameterObject();
$paramObj->first = true;
$paramObj->last = true;
foo($paramObj);
Note! You can modify the object to use method for setting parameters which will provide you with possibility of chaining if you return $this in every set method. So the function call would like like this:
$paramObj = new ParameterObject();
foo($paramObj->setFirst(true)->setSecond(true));
maybe
foo(true, false, false, true);
or change the position of arguments like
function foo($first, $last, $second=false, $third=false)
foo(true, true);
?
if you want to use last argument of function in php you must enter all argument before it and you can't use name of arguments when call functions. in some language like swift can call function with name of argument but not in php
I am working on a custom DB Table Mapper in PHP.
Is it possible in PHP to make something like "virtual methods" to access the properties? Like Methods, that don't really exist.
For Example: A class "user" has the property "$name", i don't want to create a "Get" Method for this one, but i want to access the property via a virtual Method, like this: $user->GetName();
I was thinking of working with Conventions. So everytime a "virtual" Method has been called, you catch it, and check if it has the prefix "Get" or "Set".
If it has the prefix "Get" you strip the part after "Get" and make it lowercase, so you have the property you want to access.
My Idea (Pseudo Code):
public function VirtualMethodCalled($method_name)
{
//Get the First 3 Chars to check if Get or Set
$check = substr($method_name, 0, 3);
//Get Everything after the first 3 chars to get the propertyname
$property_name = substr($method_name, 3, 0);
if($check=="Get")
{
return $this->{$property_name};
}
else if($check=="Set")
{
$this->{$property_name};
$this->Update();
}
else
{
//throw exc
}
}
You can use a magic method to achieve this, example:
class A {
private $member;
public function __call($name, $arguments) {
//Get the First 3 Chars to check if Get or Set
$check = substr($method_name, 0, 3);
//Get Everything after the first 3 chars to get the propertyname
$property_name = substr($method_name, 3);
if($check=="Get")
{
return $this->{$property_name};
}
else if($check=="Set")
{
$this->{$property_name} = $arguments[0]; //I'm assuming
}
else
{
//throw method not found exception
}
}
}
I'm mainly using the code you provided for the contents. You can obviously extend this to also handle things like function name aliases or whatever you need.
Consider this scenario where I want to SOMETIMES pass data through to my view:
public function show($orderId)
{
if ($sometimesTrue == true)
{
$optionalParameter = 'optional parameter';
}
return view('show', compact([$a, $b, $c, $optionalParameter]));
}
$optionalParameter is not always set, so I want to know what my options are for setting individual, optional view parameters without re-arranging the structure of the function.
In Zend, the following is possible:
$this->view->optionalParameter = $optionalParameter;
Which can go anywhere in the controller method, not just at the end where the view is instantiated. Of course, back in Laravel, I could do something this:
public function show($orderId)
{
$paramaterArray = [$a, $b, $c];
if ($sometimesTrue == true)
{
$optionalParameter = 'optional parameter';
$paramaterArray[] = $optionalParameter;
}
return view('show', compact($paramaterArray));
}
But re-arranging entire functions because a optional parameter is introduced seems a bit limiting. Is there any way I can set an individual parameter for a view?
You can just built your own protected function + protected property in the Controller class. You could do something like this:
Beneath is using the splat operator so it will only work in php 5.6 >=
protected $optionalParameter;
protected function optionalcompact(...$parameters) {
if(!empty($this->optionalParameter)){
return compact($parameters, $this->optionalParameter);
} else {
return compact($parameters);
}
}
Then back in your own built controller class you can do this:
public function show($orderId)
{
if ($sometimesTrue == true)
{
$this->optionalParameter = 'optional parameter';
}
return view('show', $this->optionalcompact($a, $b, $c));
}
I would simply use the following
public function show($orderId)
{
$paramaterArray = [$a, $b, $c];
$paramaterArray['optional'] = $sometimesTrue == true ? 'optional parameter' : '';
return view('show', $paramaterArray);
}
Since this is an optional parameter so I don't need to check every time in my view whether it's set or not, simply {{ $optional }} will work better, it'll be printed if any value is set or nothing will be printed if the $optional variable is empty. This way, I'll remain consistant.
Also, you may check the Larave's View Composers, it may help you.
Researching further, it seems that using compact with the names of the variables (as opposed to the variables themselves) will silently ignore the missing variable which gives the intended behaviour:
$var1 = 'bob';
$var2 = 'nigel';
var_dump(compact('var1', 'var2', 'var3'));
Returns:
array(2) { ["var1"]=> string(3) "bob" ["var2"]=> string(5) "nigel" }
Which works perfectly for my scenario. Using it this way, the controller method doesn't require refactoring and no additional coding is required. Whether relying on compact to not issue a warning is good coding practice is another question.
Just to confirm, calling compact with the variables themselves WILL throw a notice warning:
$var1 = 'bob';
$var2 = 'nigel';
var_dump(compact($var1, $var2, $var3));
Returns:
NOTICE Undefined variable: var3 on line number 4