Add to Laravel query using callable - php

I want to add to a Laravel query using a callable.
For example, I have a string for sorting records. Based on this string, I want to add to my query via an array:
public $sort = 'Newest';
public function sortQuery(Builder $query)
{
return [
'Name' => $query->orderBy('name'),
'Newest' => $query->orderBy('created_at'),
'Oldest' => $query->orderByDesc('created_at'),
];
}
public function paginateQuery()
{
$query = User::query();
foreach ($this->sortQuery($query) as $key => $value) {
if ($this->sort == $key) {
$query = $value;
}
}
return $query->paginate();
}
In this example, when I run $this->paginateQuery() it does not sort as desired.
I've also tried $query = $this->sortQuery($query)[$this->sort]; instead of the foreach loop and the result is the same.
How would I chain $value onto the $query based on the array key?

You can amend your function slightly to apply the sorting immediately, for instance like so:
public function sortQuery(Builder $query, $sortKeys)
{
// Define a map to find the options for your specific sorting key.
$map = [
'Name' => ['name', 'ASC'],
// Note: I flipped this around, Newest first means "descending date".
'Newest' => ['created_at', 'DESC'],
'Oldest' => ['created_at', 'ASC'],
];
// Loop the given sortkeys. The (array) cast allows you to pass a string as well.
foreach((array) $sortKeys as $sortKey) {
// Check if map exists.
if(isset($map[$sortKey])) {
// Use the splat operator to pass the map values as arguments to the orderBy function
// (the second argument can be ASC/DESC)
$query->orderBy(...$map[$sortKey]);
}
}
You can even define it as a query scope in one of your models (preferably a parent class that is inherited by multiple Eloquent models), see https://laravel.com/docs/8.x/eloquent#local-scopes:
// Model.php
/**
* The prefix `scope` is required. It can be called as `$query->sortMe(...)`.
*/
public function scopeSortMe(Builder $query, $sortKeys)
{
// ... same code
}
// SomeController.php
// Calling it only requires the additional parameters, not the $query object.
$query->sortMe($sortKeys);

Related

PHP - iterate through different value types

I have object of class $values like:
Array
(
[0] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => CONNECT_NETWORKS_ON_SIGN_UP
[value:App\ValueObject\Features:private] => 1
)
[1] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => SHOW_BILLING
[value:App\ValueObject\Features:private] => 1
)
[2] => App\ValueObject\Features Object
(
[feature:App\ValueObject\Features:private] => FEATURE_FLAGS
[value:App\ValueObject\Features:private] => 'activity'
)
)
All array keys are returning boolean type value expect one, which returns string value.
My result with the code:
$arrays = array_map(
function($value) { return [strtolower((string) $value->getFeature())]; },
iterator_to_array($values)
);
return array_merge(...$arrays);
returns list of feature names like:
"features": [
"connect_networks_on_sign_up",
"show_billing",
"feature_flags"
]
What I want to edit is that for the last one we write its value NOT feature name ($value->getValue())
I am assuming that using in_array() PHP function would be the best approach here but I can't find a way to use it within my current method.
Tried with foreach() loop but nothing happens, like it's something wrong:
$features = [];
foreach ($values as $value)
{
$setParam = $value->getFeature();
if ($value == 'FEATURE_FLAGS') {
$setParam = $value->getValue();
}
$features[] = strtolower((string) $setParam);
}
return $features;
Can someone help?
Thanks
You should probably operate on the feature code FEATURE_FLAGS, rather than assuming that the last feature in the array always contains the flags. Using your existing code, that could be as simple as:
$arrays = array_map(
function($value)
{
/*
* If the current Features object has the feature code FEATURE_FLAGS,
* return the value itself, otherwise return the feature code in lowercase
*/
return ($value->getFeature() == 'FEATURE_FLAGS') ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
If you want to define an array of feature codes that you need to treat this way, you can define it internally in the callback, but it is probably a better idea to define it externally. You can then pass it into the callback with use
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
Working example:
// Mock the Features class
class Features
{
private $feature;
private $value;
public function __construct($feature, $value)
{
$this->feature = $feature;
$this->value = $value;
}
public function getFeature()
{
return $this->feature;
}
public function setFeature($feature): void
{
$this->feature = $feature;
}
public function getValue()
{
return $this->value;
}
public function setValue($value): void
{
$this->value = $value;
}
}
// Mock an iterator with test Feature instances
$values = new ArrayIterator( [
new Features('CONNECT_NETWORKS_ON_SIGN_UP', 1),
new Features('SHOW_BILLING', 1),
new Features('FEATURE_FLAGS', 'activity')
]);
/*
* Define an array of feature codes that we want to return
* values for
*/
$valueCaptureFeatures = ['FEATURE_FLAGS'];
$arrays = array_map(
function($value) use ($valueCaptureFeatures) // <-- Put our $valueCaptureFeatures in the scope of the callback
{
/*
* If the current Features object has a feature code in the $valueCaptureFeatures array,
* return the value itself, otherwise return the feature code in lowercase
*/
return (in_array($value->getFeature(), $valueCaptureFeatures)) ? [$value->getValue()]:[strtolower((string) $value->getFeature())];
},
iterator_to_array($values)
);
$output = array_merge(...$arrays);
$expectedResult = [
'connect_networks_on_sign_up',
'show_billing',
'activity'
];
assert($output == $expectedResult, 'Result should match expectations');
print_r($output);

Laravel Model Dynamic Attribute

I would like to ask how it's possible to create a dynamic attribute on the model class. Let's suppose I have a table structure like below code.
Schema::create('materials', function (Blueprint $table) {
$table->increments('id');
$table->string('sp_number');
$table->string('factory');
$table->text('dynamic_fields')->comment('All description of the material will saved as json');
$table->timestamps();
});
I have a column in my table structure named "dynamic_fields" that will hold a JSON string for the fields. An example of JSON structure below.
[
{
"name":"COLOR WAY",
"value":"ASDFF12"
},
{
"name":"DESCRIPTION",
"value":"agg2sd12"
},
{
"name":"REF NUM",
"value":"121312"
}
]
I want to access a field from my dynamic fields, like for example "COLOR WAY".
In my model I want to access the "COLOR WAY" field on the dynamic field like this way
$material->color_way;
Can anybody show me how to do it?
If you know that there will only be certain dynamic fields ahead of time, you could opt to create accessor methods for them. For example, you could add this to your model:
// Dynamic fields must be cast as an array to iterate through them as shown below
protected $casts = [
'dynamic_fields' => 'array'
];
// ...
public function getColorWayAttribute()
{
foreach ($this->dynamic_fields as $field) {
if ($field['name'] === 'COLOR WAY') {
return $field['value'];
}
}
return null;
}
This will allow you to do:
$colorWay = $material->color_way;
Alternatively, if the combinations your dynamic_fields are not limited, there could be a large number of them or you want there to be more flexibility to be able to add more and have them accessible, you could override the getAttribute method of Laravel's model class.
// Dynamic fields must be cast as an array to iterate through them as shown below
protected $casts = [
'dynamic_fields' => 'array'
];
// ...
public function getAttribute($key)
{
$attribute = parent::getAttribute($key);
if ($attribute === null && array_key_exists('dynamic_fields', $this->attributes)) {
foreach ($this->dynamic_fields as $dynamicField) {
$name = $dynamicField['name'];
if (str_replace(' ', '_', mb_strtolower($name)) === $key) {
return $dynamicField['value'];
}
}
}
return $attribute;
}
This approach calls Laravel's implementation of getAttribute which first checks if you have an actual attribute defined, or if you have an accessor defined for the attribute (like in my first suggestion), then checks if a method exists with that name on the base model class and then finally attempts to load a relation if you have one defined.
When each of those approaches fails (null is returned), we then check to see if there's a dynamic_fields attribute in the model. If there is, we loop through each of the dynamic fields (assuming your dynamic_fields is cast as an array), we then convert the name of the defined dynamic field to lowercase and replace spaces with underscores. We then finally check to see if the name we have just derived matches the key provided and if it does, we return the value. If it doesn't, the original $attribute will be returned, which will be null.
This would allow you to get any of your dynamic fields as if they were defined as attributes in the class.
$colorWay = $material->color_way;
$description = $material->description;
$refNum = $material->ref_num;
Please note: I have not tested this code, there could well be an issue or two present. Give it a try and see if it works for you. Also note that this will only work for getting dynamic fields, setting them will require overriding another method.
Try to use this code in your model:
protected $casts = [
'dynamic_fields' => 'array',
];
public function setAttribute($key, $value)
{
if (!$this->getOriginal($key)) {
$this->dynamic_fields[$key] = $value;
}
parent::setAttribute($key, $value);
}
public function getAttribute($key)
{
if (!$this->getOriginal($key)) {
return $this->dynamic_fields[$key]
}
parent::getAttribute($key);
}
In this example, you can get Dynamic Column form Dynamic Model. as well as its Models Relation too
1) first you have to define a table Scope in Model.
private $dynamicTable='';
public function scopeDefineTable($query,$tableName)
{
if( $tableName )
{
$this->dynamicTable= $tableName;
}
else
{
$this->dynamicTable= "deviceLogs_".date('n')."_".date('Y');
}
$query->from( $this->dynamicTable );
$this->table=$this->dynamicTable; # give dynamic table nam to this model.
}
public function scopeCustomSelect( $query ,$items=[])
{
$stu_class_col=['id as stu_class_id','std_id']; // Required else retional model will not retun data. here id and std_id is primary key and foreign key.
$stu_doc_col=['id as stu_doc_id','std_id'];// Required else retional model will not retun data. here id and std_id is primary key and foreign key.
foreach ( $items as $col)
{
if( Schema::hasColumn('student_information', $col ))
{
$stu_info_col[]= $col ;
}
elseif ( Schema::hasColumn('student_class',$col))
{
$stu_class_col[]= $col ;
}
elseif ( Schema::hasColumn('student_image',$col))
{
$stu_doc_col[]= $col ;
}
}
// converting array to string for bind column into with relation...
$stu_class_col_string = implode(',',$stu_class_col);
$stu_doc_col_string = implode(',',$stu_doc_col);
return $colQuery = $query->select($stu_info_col)
->with(["student_class:$stu_class_col_string", "studentImage:$stu_doc_col_string"]);
}
using this you can get data from Rational Model too...
from Controller
$studentInfo = Student::whereHas("student_class",function($q) use($req){
$q->where("std_session",$req->session_code);
$q ->where("std_class",$req->class_code);
$q ->where("std_section",$req->std_section); })
->customSelect($fields['dataList'])
->get();
here I am not using dynamic Model Scope. only Dynamic SustomSelect scope..

Where condition with multiple result set on ZF2 AbstractTableGateway

In Zf2 application written the model file to retrieve the data set from the table,
it works as expected for returning one result set, but for returning multiple rows not able to achieve by the below code.
Working and Returning single row
/**
* #param $id
* #return bool|Entity\Feeds
*/
public function getAppFeed($id)
{
$row = $this->select(array('app_id' => (int)$id))->current();
if (!$row)
return false;
$feedVal = new Entity\Feeds(array(
'id' => $row->id,
'title' => $row->title,
'link' => $row->link,
'Description' => $row->description,
'created' => $row->created,
));
return $feedVal;
}
Removed current and tried tablegateway object also but throwing the error.
Feeds table will have multiple record for each of the application, I need a function to achieve the same.
The Select always returns a ResultSet. You can access the objects(1) of ResultSet by iterating over it, because it implements the Iterator Interface.
Just an example piece of code:
public function getAppFeed($id)
{
$resultSet = $this->select(array('app_id' => (int)$id));
if ($resultSet instanceof \Zend\Db\ResultSet) {
foreach($resultSet as $item) {
// do your feed stuff here
// e.g. $item->id
}
} else {
return false;
}
}
(1) Object: meaning whatever object you asigned as Prototype in your TableGateway.
For further details, please checkout the documentation of ResultSet.

Lithium: How to manually populate a DocumentSet?

How can I return data from an external source as a DocumentSet?
I set up a custom data source to interface with Amazon's Product Advertising API. To do this, I subclassed lithium\data\source\Http and redefined the read method to suit my needs as described in the documentation (http://li3.me/docs/manual/working-with-data/creating-data-sources.wiki).
However, my lithium version (0.11, last release) does not seem to have a cast method like in the example and if I create one it won't get called when I do return $this->item($model, $data, $options).
So, I made a custom item function to create the Documents by calling parent::item just like the documentation example does for cast.
Then, after the recursive calls, I end up with an array of Document objects and the final call to parent::item then gives me an empty DocumentSet object.
How should I pass the data on to create a proper DocumentSet?
Here's a minimal example of my code:
// Within class Amazon extends \lithium\data\source\Http
protected function _init() {
// Define entity classes.
$this->_classes += array(
'entity' => 'lithium\data\entity\Document',
'set' => 'lithium\data\collection\DocumentSet'
);
parent::_init();
}
public function read($query, array $options = array()) {
// Extract from query object.
$parameters = $query->export($this, array('keys' => array('conditions')));
$conditions = $parameters['conditions'];
// Code stripped to validate conditions and prepare Amazon request (that part works).
// results in a $queryString variable.
// Get response from Server.
$xml = simplexml_load_string($this->connection->get($this->_config['basePath'], $queryString));
// Stripped response validation and reformatting -> $items contains an array of SimpleXMLElement objects.
return $this->item($query->model(), $items, array('class' => 'set'));
}
public function item($model, array $data = array(), array $options = array()) {
// Recursively create Documents for arrays.
foreach($data as $key => $value) {
if(is_array($value)) {
$data[$key] = $this->item($model, $value, array('class' => 'entity'));
}
else if(is_object($value) && get_class($value) == "SimpleXMLElement") {
// Stripped code to extract data from XML object and put it in array $docData.
$data[$key] = $this->item($model, $docData, array('class' => 'entity'));
}
}
// Works perfectly for every (recursive) call with $options['class'] == 'entity' but fails for the final call with $options['class'] == 'set' (for this final call $data contains an array of Document objects).
return parent::item($model, $data, $options);
}
I would track the master branch instead of the release versions.
In your case, since you're boxing your objects manually, I would do something like:
return $this->_instance('set', compact('data'));

How to get values that doesn't pass from FilterIterator

I'm using FilterIterator to filter out the values and implemented the accept() method successfully. However I was wondering how would it be possible to get the values that returned false from my accept method in single iteration. Let's take the code below as an example (taken from php.net);
class UserFilter extends FilterIterator
{
private $userFilter;
public function __construct(Iterator $iterator , $filter )
{
parent::__construct($iterator);
$this->userFilter = $filter;
}
public function accept()
{
$user = $this->getInnerIterator()->current();
if( strcasecmp($user['name'],$this->userFilter) == 0) {
return false;
}
return true;
}
}
On the code above, it directly filters out the values and returns the values that pass from the filteriterator. Implemented as;
$array = array(
array('name' => 'Jonathan','id' => '5'),
array('name' => 'Abdul' ,'id' => '22')
);
$object = new ArrayObject($array);
$iterator = new UserFilter($object->getIterator(),'abdul');
It will contain only the array with name Jonathan. However I was wondering would it be possible to store the object with name Abdul in another variable using the same filter with a slight addition instead of reimplementing the entire filter to do the opposite?. One way I was thinking would exactly copy paste the FilterIterator and basically change values of true and false. However are there any neat ways of doing it, since it will require another traversal on the list.
I think you must rewrite the accept() mechanic. Instead of returning true or false, you may want to break down the array to
$result = array(
'passed' => array(...),
'not_passed' => array(...)
);
Your code may look like this
if (strcasecmp($user['name'], $this->userFilter) == 0) {
$result['not_passed'][] = $user;
} else {
$result['passed'][] = $user;
}
return $result;

Categories