Laravel Model Dynamic Attribute - php

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..

Related

Add to Laravel query using callable

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

How dou you write/call 2 tables in $this->db->table('table_name') in Codeigniter 4

Hello i have 2 tables that i want to call right now, for the EDIT (part of the CRUD)
tables:
table_a
table_b
i found in youtube how to update/edit from 2 tables, i need to call bot of the tables.
here's the code for the model
public function edit_this($ID_A)
{
return $this->db->table('table_a', '*i don't know how to insert the 2nd table')->where('ID_A', $ID_A)->get()->getRowArray();
}
Here's the controller
public function this_edit($ID_A)
{
$data = [
'title' => 'Admin',
'navbartitel' => 'You know this',
'alledit' => $this->theModel->edit_this($ID_A),
'validation' => \Config\Services::validation()
];
return view('this/all/edit', $data);
}
it works but i only can accsess the tabel_a, but i need them both so i can show what i've written in the edit form, from the database
anyone can help? thank you
$this->db->table(...) returns an instance of QueryBuilder and will happily accept a single string of comma-separated tables ("table1, table2..."), or even an array for that matter (['table1', 'table2'...]), as its first parameter. You are doing neither and instead passing multiple parameters.
When you call table(), the value passed in the first parameter is used during the creation of the database-specific Builder class:
public function table($tableName)
{
if (empty($tableName))
{
throw new DatabaseException('You must set the database table to be used with your query.');
}
$className = str_replace('Connection', 'Builder', get_class($this));
return new $className($tableName, $this);
}
The DB-specific Builder class has no constructor of its own so falls back on the __construct defined in BaseBuilder, which it extends:
public function __construct($tableName, ConnectionInterface &$db, array $options = null)
{
if (empty($tableName))
{
throw new DatabaseException('A table must be specified when creating a new Query Builder.');
}
$this->db = $db;
$this->from($tableName);
...
I've truncated this for brevity because the important part is that call to $this->from, which is in the end how multiple tables get processed:
public function from($from, bool $overwrite = false)
{
if ($overwrite === true)
{
$this->QBFrom = [];
$this->db->setAliasedTables([]);
}
foreach ((array) $from as $val)
{
if (strpos($val, ',') !== false)
{
foreach (explode(',', $val) as $v)
{
$v = trim($v);
$this->trackAliases($v);
$this->QBFrom[] = $v = $this->db->protectIdentifiers($v, true, null, false);
}
}
else
{
$val = trim($val);
// Extract any aliases that might exist. We use this information
// in the protectIdentifiers to know whether to add a table prefix
$this->trackAliases($val);
$this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
}
}
return $this;
}

If Class exists && If Method exists PHP / Laravel

PHP/Laravel
Hey, I'm moving into abstraction in php and am attempting to validate and store values based on whatever has been submitted, where I expect that the methods should neither know what to validate against and/or which class and method to use to do so -
What I've got works but I can see that there would be issues where classes/methods do not exist. Here lays my question.
If I were to call a method in the following format, which way would be best to 'check' if class_exists() or the method exists()?
public function store(Request $request)
{
$dataSet = $request->all();
$inputs = $this->findTemplate();
$errors = [];
$inputValidators = [];
foreach ($inputs as $input) {
$attributes = json_decode($input->attributes);
if (isset($attributes->validate)) {
$inputValidators[$input->name] = $input->name;
}
}
foreach ($dataSet as $dataKey => $data) {
if (array_key_exists($dataKey, $inputValidators)) {
$validate = "validate" . ucfirst($dataKey);
$validated = $this->caseValidator::{$validate}($data);
if ($validated == true) {
$inputValidators[$dataKey] = $data;
} else {
$errors[$dataKey] = $data;
}
} else {
$inputValidators[$dataKey] = $data;
}
}
if (empty($errors)) {
$this->mapCase($dataSet);
} else {
return redirect()->back()->with(['errors' => $errors]);
}
}
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}
For some additional context, an input form will consist of a set of inputs, this can be changed at any time. Therefore I am storing all values as a json 'payload'.
When a user submits said form firstly the active template is found, which provides details on what should be validated $input->attributes, once this has been defined I am able to call functions from caseValidator model as $this->caseValidator::{$validate}($data);.
I do not think that any checks for existence will be needed here as the validation parameters are defined against an input, thus if none exist this check will be skipped using if (array_key_exists($dataKey, $inputValidators))
However, I am dispersing some data to other tables within the second block of code using mapCase(). This is literally iterating over all array keys regardless of if a method for it exists and thus the initial check cannot be made as seen in the first block. I've attempted to make use of class_exists() and method_exists but logically it does not fit and I cannot expect them to work as I'd like, perhaps my approach in mapCase is not correct? I guess if I'm defining a class for each key I should instead use one class and have methods exist there, which would remove the need to check for the class existing. Please advise
Reference:
$attribute = $this->{$model}::{$method}($dataKey);
Solved the potential issue by using class_exists(), considering I know the method names as they are the same as the $dataKey.
public function mapCase($dataSet)
{
foreach($dataSet as $dataKey => $data) {
$model = 'case' . ucfirst($dataKey);
if (class_exists("App\Models\CaseRepository\\" . $model)) {
$method = 'new' . ucfirst($dataKey);
$attribute = $this->{$model}::{$method}($dataKey);
}
if($attribute == false) {
return redirect()->back()->with(['issue' => 'error msg here']);
}
}
return redirect()->back->with(['success' => 'success msg here'])'
}

CakePHP 3.5 Always apply asText() MySQL function to Spatial field

I have a custom PolygonType which represents a POLYGON() field in a MySQL table.
class PolygonType extends BaseType implements ExpressionTypeInterface
{
public function toPHP($value, Driver $d)
{
// $value is binary, requires unpack()
}
}
I can use $query->func()->astext() on every find, but I would like to know if it's possible to always apply MySQL's AsText() function when selecting this field instead (similar to how toExpression() can be used when inserting data).
AFAIK there is no such functionality, type classes and select clause contents never touch.
If you wanted to apply this to all finds, then you could for example use the Model.beforeFind() event, traverse the select clause and transform the fields to expressions. Here's a quick and dirty example, where field is the name of the POLYGON type column:
// in the respective table class
use Cake\Event\Event;
use Cake\ORM\Query;
// ...
public function beforeFind(Event $event, Query $query, \ArrayObject $options, $primary)
{
$query->traverse(
function (&$value) use ($query) {
if (empty($value)) {
$value = $query->aliasFields($this->getSchema()->columns());
}
foreach ($value as $key => $field) {
if (is_string($field) &&
$this->aliasField($field) === $this->aliasField('field')
) {
unset($value[$key]);
$value[key($query->aliasField($field))] = $query->func()->AsText([
$this->aliasField('field') => 'identifier'
]);
}
}
},
['select']
);
}
You may have to account for $field as expressions too, in case the field might be used in one and needs to be converted there too.
Another way would be to convert the data on PHP level in the type class' toPHP() method, as already indicated in your code example.
See also
Cookbook > Database Access & ORM > Table Objects > Lifecycle Callbacks > beforeFind
API > \Cake\Database\Query::traverse()
Based on ndp's answer, it's possible to inspect the field types via $query->getDefaultTypes() and apply a SQL function as required. However, $value is empty if no fields are initially stated (e.g. when using Table::get() so there's also a check for this.
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
{
$query->traverse(
function (&$value) use ($query) {
if (is_array($value) && empty($value)) {
$query->all();
}
$defaultTypes = $query->getDefaultTypes();
foreach ($value as $key => $field) {
if (in_array($defaultTypes[$field], ['point', 'polygon'])) {
$value[$key] = $query->func()->astext([
$this->aliasField($field) => 'identifier'
]);
}
}
$query->select($value);
},
['select']
);
}

Laravel4 - Track changes to be saved by Eloquent

In Laravel 4, I have a class extending Eloquent and I need to record changes (to keep the history) at the time of saving.
Saving event in boot function is called as expected. The question is: how do I know which fields were changed and are about to be saved? Also, can I access existing values without loading the record again?
I know, one way could be to load the record again and compare all fields one by one. Is there any better optimized way to do that?
class Record extends Eloquent {
protected static function boot()
{
parent::boot();
static::saving(
function($record)
{
// It runs properly. This is where changes should be compared
return true;
}
);
}
}
Thank you.
Eloquent's getDirty() and getOriginal() did the trick:
static::saving(
function($record)
{
$dirty = $record->getDirty();
foreach ($dirty as $field => $newdata)
{
$olddata = $record->getOriginal($field);
if ($olddata != $newdata)
{
// save changes from $olddata to $newdata
}
}
return true;
}
);
I don't think accepted answer is the best way to do that. Why would you loop through all changed fields to figure out what has changed? Laravel has this two Model methods that can help you better,
on save function/route/controller whichever you use
$model::find(X);
...
'''
$model->fill(Input::all());
if( !empty($model->getDirty() )
// Get status of model. if nothing has changed there is no need to get in here
{
if($model->isDirty( { mixed data (array,string,null) } ))
// isDirty() will help you to find out if the field is different than original
{
.....
enter code here
....
}
}
You can use events:
Event::listen('saving', function($model)
{
// Do whatever you need to do with your $model before saving or not:
return true;
});
You can also hook that saving method to any controller action:
Event::listen('saving', 'LogThings#saving');
I have something like this in my project.
Record::updating(function($record){
$original = $record->getOriginal();
foreach($original as $index => $value){
if($index != 'updated_at' AND $index != 'created_at'){
if($value != $record[$index]){
RecordUpdate::create(array('record_id' => $record->id, 'fieldname' => $index, 'old_value' => $value, 'new_value' => $record[$index], 'created_at' => date('Y-m-d H:i:s')));
}
}
}
});
The Eloquent class that you're extending has getOriginal() and getAttributes() methods to get the originally-hydrated properties and the currently set properties, respectively.
So, in your saving event you can do something like...
if ( ! empty( array_diff( $record->getOriginal(), $record->getAttributes() ) ) )
{
//log changes
}
If you want to do this for all/most of your Models, I suggest abstracting this out to a generic Model Observer:
////
/* Subclassable, generic Model Observer */
////
class ModelObserver {
public function saving($model)
{
if ( ! empty( array_diff( $record->getOriginal(), $record->getAttributes() ) ) )
{
//log changes
}
}
}
////
/* Set up observers, for example in start/global.php */
////
$modelObserver = new ModelObserver;
Comment::observe($modelObserver);
Post::observe($modelObserver);
User::observe($modelObserver);

Categories