Laravel use dynamic mutators in where clause - php

So i have two columns - SizeX & SizeY. For front end users I use Laravel mutator
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
To format the sizes like this SizeX x SizeY. The column Sizes does not exists because its dynamic. Is it possible to use mutators or alternative within the Elaquent model to detect that this is a dynamic attribute, and use some sort of method to convert the SizeX x SizeY to individual columns? Once the user submits the attribute Size back to laravel application for filtering?
Edit:
Right, this is the method I'm using to retrieve filtered Items
public function scopeFilteredMaterials($query,$params = array()){
/**
* Get filters
*/
$filters = array();
/**
* Extract Info
*/
if(!empty($params) && is_array($params)){
/**
* Get Available Filters
*/
$filters = $this->getAvailableFilters();
foreach ($filters as $key => $filter){
if(isset($params[$filter])){
$filters[$filter] = $params[$filter];
unset($filters[$key]);
}else{
unset($filters[$key]);
}
}
}
foreach ($filters as $key => $filter){
$query->whereIn(key ,$filter);
}
$result = $query->get();
}
This is the filters variable which holds available filters for user to see
protected $filters = array(
"Name",
"url",
"Size",
);
I'm using the above to show the specific values to the user. Once the user selects those values I'm using the same array to check against those filters and fire the query. My problem is the Size attribute is made up of two columns which I have not problem using the following Mutator and $appends variable to automatically bring the value to the user.
/**
* Get Size Attribute
*/
public function getSizeAttribute(){
/**
* Set Size
*/
$size = $this->SizeX. " x ". $this->SizeY;
/**
* Return
*/
return $size;
}
But i ca't figure out a way to convert the Size variable back to SizeX & SizeY

If you are always creating the composite variable through the accessor, then you can use a scope that parses this string. Something like this:
public function scopeSize($query, $size)
{
$sizes = explode(' x ', $size);
return $query->where('SizeX', $sizes[0])->where('SizeY', $sizes[1]);
}
You can then use this scope in the filter method or anywhere else.

If you want to create a new dynamic attribute, just create an accessor:
public function getSizesAttribute()
{
return $this->SizeX . ' x ' . $this->SizeY;
}
If you want to set SizeX and SizeY automatically in case if Sizes property exists. In this case, you can create mutators for both SizeX and SizeY. For example:
public function setSizeXAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeX'] = explode(' x ', $this->attributes['Sizes']))[0];
}
}
public function setSizeYAttribute($value)
{
if (!empty($this->attributes['Sizes'])) {
$this->attributes['SizeY'] = explode(' x ', $this->attributes['Sizes']))[1];
}
}
PS: This is a solution for standard snake_case properties, like size_x. I'd really recommend you to use these in Laravel. If you still want to use SizeX, you should also do this.

Related

Is there a quicker way to check is a LDAP user is in a LDAP Group? This is slow as it checks each one each time?

Using adldap-laravel and Laravel 5.8.
I'm getting permissions based on LDAP groups. I can check if a user is part of a group using: $user->ldap->inGroup('Accounts'); (that returns a bool)
However that method also accepts an array, but seems to be an "AND" search, rather than "ANY".
So I've written this:
/**
* LDAP helper, to see if the user is in an AD group.
* Iterate through and break when a match is found.
*
* #param mixed $input
* #return bool
*/
public function isInGroup($input)
{
if (is_string($input)) {
$input[] = $input;
}
foreach ($input as $group)
if ($this->ldap->inGroup($group)) {
return true;
}
return false;
}
Implemented like this:
$user->isInGroup(['Accounts', 'Sales', 'Marketing']);
However it takes a long time to check.
Does anyone know of an improved way to solve my problem?
Yes can do it via query builder of Adldap.
/**
* LDAP helper, to see if the user is in an AD group.
* Iterate through and break when a match is found.
*
* #param mixed $input
* #return bool
*/
public function isInGroup($input){
if (is_string($input)) {
$input[] = $input;
}
$counter = 0;
$groupQuery = $this->ldap->search()->groups();
foreach ($input as $group) {
if ($counter == 0) {
$groupQuery->where('samaccountname', '=', $group);
} else {
$groupQuery->orWhere('samaccountname', '=', $group);
}
$counter++;
}
return $groupQuery->get()->count();
}
This samaccountname may be different field name for your LDAP provider. I couldn't tested it if has any syntax or method name error anyway you will find this same methods from your Adldap Provider class. The algorithm/process is same.

CakePHP3: How to change an association strategy on-the-fly?

I would like to change an association strategy (hasMany) on the fly to "in" (default) to "select". Because this will correct the result for this situation:
"Get all publishers and only the first five books":
$publishersTable = TableRegistry::getTableLocator()->get('Publishers');
$publishersTable->getAssociation('Books')->setStrategy('select');
$query = $publishersTable->find()
->contain(['Books'=> function(Query $q){
return $q->limit(5);
}]);
Unfortunately, Cake still using "in" to run the query and not "separated queries" and the result is only 5 publishers (and not all publishers with the first 5 books).
Is it possible to change the strategy on-the-fly?
Thanks in advance !
A hasMany association will always use a single separate query, never multiple separate queries. The difference between the select and subquery strategies is that one will directly compare against an array of primary keys, and the other against a joined subquery that will match the selected parent records.
What you are trying is to select the greatest-n-per-group, that's not possible with the built in association loaders, and it can be a little tricky depending on the DBMS that you are using, check for example How to limit contained associations per record/group? for an example for MySQL < 8.x using a custom association and loader.
For DBMS that do support it, look into window functions. Here's an example of a loader that uses native window functions, it should be possible to simply replace the one in the linked example with it, but keep in mind that it's not really tested or anything, I just had it laying around from some experiments:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* #var int
*/
protected $limit;
/**
* The target table.
*
* #var \Cake\ORM\Table
*/
protected $target;
/**
* {#inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {#inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {#inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* #var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}
Thanks #ndm but I found another shorter solution:
$publishersTable->find()
->formatResults(function ($results) use ($publishersTable) {
return $results->map(function ($row) use ($publishersTable) {
$row['books'] = $publishersTable->Books->find()
->where(['publisher_id'=>$row['id']])
->limit(5)
->toArray();
return $row;
});
});

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

Unit test: using the proper terminology for mocking/stubbing

After fundamental changes on my project system architecture, I find myself in a situation where I would need to create "fake" implementation in order to test some functionality that used to be public like the following:
/**
* Display the template linked to the page.
*
* #param $newSmarty Smarty object to use to display the template.
*
* #param $parameters associative Array containing the values to pass to the template.
* The key is the name of the variable in the template and the value is the value of the variable.
*
* #param $account child class in the AccountManager hierarchy
*
* #param $partialview String name of the partial view we are working on
*/
protected function displayPageTemplateSmarty(Smarty &$newSmarty, array $parameters = array(), AccountManager $account = NULL, string $partialview = "")
{
$this->smarty = $newSmarty;
if (is_file(
realpath(dirname(__FILE__)) . "/../../" .
Session::getInstance()->getCurrentDomain() . "/view/" . (
!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
)
)) {
$this->smarty->assign(
'activeLanguage',
Session::getInstance()->getActiveLanguage()
);
$this->smarty->assign('domain', Session::getInstance()->getCurrentDomain());
$this->smarty->assign(
'languages',
Languagecontroller::$supportedLanguages
);
$this->smarty->assign(
'title',
Languagecontroller::getFieldTranslation('PAGE_TITLE', '')
);
$this->smarty->assign_by_ref('PageController', $this);
$htmlTagBuilder = HTMLTagBuilder::getInstance();
$languageController = LanguageController::getInstance();
$this->smarty->assign_by_ref('htmlTagBuilder', $htmlTagBuilder);
$this->smarty->assign_by_ref('languageController', $languageController);
if (!is_null($account)) {
$this->smarty->assign_by_ref('userAccount', $account);
}
if (!is_null($this->menuGenerator)) {
$this->smarty->assign_by_ref('menuGenerator', $this->menuGenerator);
}
foreach ($parameters as $key => $value) {
$this->smarty->assign($key, $value);
}
$this->smarty->display((!empty($partialview) ?
"partial_view/" . $partialview :
str_replace(
array(".html", "/"),
array(".tpl", ""),
Session::getInstance()->getActivePage()
)
));
}
}
In this case, the PageController class used to be called directly in controllers, but is now an abstract class extended by the controllers and my unit tests can no longer access the method.
I also have methods like this one in my new session wrapper class that can only be used in very specific context and for which I really need to create fake page implementation to test them.
/**
* Add or update an entry to the page session array.
*
* Note: can only be updated by the PageController.
*
* #param $key String Key in the session array.
* Will not be added if the key is not a string.
*
* #param $value The value to be added to the session array.
*
* #return Boolean
*/
public function updatePageSession(string $key, $value)
{
$trace = debug_backtrace();
$updated = false;
if (isset($trace[1]) and
isset($trace[1]['class']) and
$trace[1]['class'] === 'PageController'
) {
$this->pageSession[$key] = $value;
$updated = true;
}
return $updated;
}
Even though I read a few article, it is still quite unclear in my mind if those fake classes should be considered as "stub" or a "mock" (or even "fake", "dummy" and so on).
I really need to use the proper terminology since my boss is expecting me (in a close future) to delegate most of my workload with oversea developers.
How would you call those fake class implementation created solely for testing purpose in order to be self-explanatory?
Gerard Meszaros explains the terminology of dummies, stubs, spies, mocks, and fakes here.
You can find examples from the PHP world here.

How to generate unique random value for each user in laravel and add it to database

I am developing a event organization website. Here when the user registers for an event he will be given a unique random number(10 digit), which we use to generate a barcode and mail it to him. Now,
I want to make the number unique for each registered event.
And also random
One solution is to grab all the random numbers in an array and generate a random number using Php rand(1000000000, 9999999999) and loop through and check all the values. Grab the first value that doesn't equal to any of the values in the array and add it to the database.
But I am thinking that there might be a better solution to this. Any suggestion?
You can use php's uniqid() function to generate a unique ID based on the microtime (current time in microseconds)
Example:
<?php
echo uniqid();
?>
Output:
56c3096338cdb
Your logic isn't technically faulty. However, if your application attracts lots of users, fetching all of the random numbers may well become unnecessarily expensive, in terms of resources and computation time.
I would suggest another approach, where you generate a random number and then check it against the database.
function generateBarcodeNumber() {
$number = mt_rand(1000000000, 9999999999); // better than rand()
// call the same function if the barcode exists already
if (barcodeNumberExists($number)) {
return generateBarcodeNumber();
}
// otherwise, it's valid and can be used
return $number;
}
function barcodeNumberExists($number) {
// query the database and return a boolean
// for instance, it might look like this in Laravel
return User::whereBarcodeNumber($number)->exists();
}
This is good:
do {
$refrence_id = mt_rand( 1000000000, 9999999999 );
} while ( DB::table( 'transations' )->where( 'RefrenceID', $refrence_id )->exists() );
To avoid the problem of having to check to see if a matching code exists every time a new one is created, I just catch MySQL's duplicate record exception (error code 1062). If that error is caught, I just call the function again until the save is successful. That way, it only has to generate a new code if it collides with an existing one. Runs a lot faster -- but obviously gets a bit slower as your number of users approaches the number of possible barcodes.
function generateBarcode($user_id) {
try {
$user = User::find($user_id);
$user->barcode = mt_rand(1000000000, 9999999999);
$user->save();
} catch (Exception $e) {
$error_info = $e->errorInfo;
if($error_info[1] == 1062) {
generateBarcode($user_id);
} else {
// Only logs when an error other than duplicate happens
Log::error($e);
}
}
}
So just loop through all the users you want to assign a code to:
foreach(User::all() as $user) {
generateBarcode($user->id);
}
You could also add some logic to escape the function loop if a maximum number of attempts are made, but I've never bothered because collisions are unlikely.
Looping through the array won't be that efficient. If your database becomes too large then it slow down the entire process and also there might be a rare situation when 2 threads are looping through the array for the same random number and it will be found available and return same number to both the tickets.
So instead of looping through the array you can set the 10 digit registration id as primary key and instead of looping through the array you can insert the registration details along with randomly generated number, if the database insert operation is successful you can return the registration id but if not then regenerate the random number and insert.
Alternate solution which will be more effective
Instead of 10 digit random numbers you can use timestamp to generate a 10 digit unique registration number and to make it random you can randomize the first 2 or 3 digits of the timestamp
One Solution could be like this:
use Illuminate\Support\Facades\Validator;
private function genUserCode(){
$this->user_code = [
'user_code' => mt_rand(1000000000,9999999999)
];
$rules = ['user_code' => 'unique:users'];
$validate = Validator::make($this->user_code, $rules)->passes();
return $validate ? $this->user_code['user_code'] : $this->genUserCode();
}
Its generating a random number between 1000000000 and 9999999999. After that, it validates the number against the table. If true then it returns the number, otherwise runs the function again.
I made something like this
/**
* Generate unique shipment ID
*
* #param int $length
*
* #return string
*/
function generateShipmentId($length)
{
$number = '';
do {
for ($i=$length; $i--; $i>0) {
$number .= mt_rand(0,9);
}
} while ( !empty(DB::table('shipments')->where('id', $number)->first(['id'])) );
return $number;
}
<?php
declare(strict_types=1);
namespace App\Helpers;
use App\Exceptions\GeneratorException;
class GeneratorHelper
{
public static $limitIterations = 100000;
/**
* #param string $column
* #param string $modelClass
* #return string
* #throws GeneratorException
*/
public static function generateID(string $modelClass, string $column): string
{
return self::run(
$modelClass,
$column,
self::IDGenerator(),
'Generation id is failed. The loop limit exceeds ' . self::$limitIterations
);
}
/**
* #param string $modelClass
* #param string $column
* #param \Generator $generator
* #param string $exceptionMessage
* #param array $whereParams
* #return string
* #throws GeneratorException
*/
protected static function run(string $modelClass, string $column, \Generator $generator, string $exceptionMessage, array $whereParams = []): string
{
try {
foreach ($generator as $id) {
$query = $modelClass::where([$column => $id]);
foreach ($whereParams as $param) {
$query->where(...$param);
}
if (!$query->first()) {
return $id;
}
}
} catch (\Throwable $e) {
$exceptionMessage = $e->getMessage();
}
throw new GeneratorException($exceptionMessage);
}
protected static function IDGenerator(): ?\Generator
{
for ($i = 1; $i <= self::$limitIterations; $i++) {
yield (string)random_int(1000000000, 9999999999);
}
return null;
}
}
sample usage
$card->number = GeneratorHelper::generateID(Card::class, 'number');
for me, I prefer using MySQL way, because when you have a large amount of data in your DB, you will have too much quires to check your number uniqueness,
for example, a code like this:
do {
$code = random_int(100000, 99999999);
}
while (AgentOrder::where("order_number", "=", $code)->exists());
so , this "do while" loop would be excueted too many times.
to avoid that, you can use MySQL way like:
private function getUniqueCodeNumber()
{
$value = AgentOrder::query()->selectRaw('FLOOR(1000000 + RAND() * 10000000) AS generatedCode')
->whereRaw("'generatedCode' NOT IN (SELECT order_number FROM agent_orders WHERE agent_orders.order_number IS NOT NULL)")
->limit(1)->first();
if ($value == null) return 100000000;
$value = (int)$value->generatedCode;
return $value;
}
this answer is inspired from this answer.
Helper (app/Helpers/Constants.php)
<?php
namespace App\Helper;
class Constants
{
public static function getUniqueId($model){
$id = mt_rand(1000000000, 9999999999);
if($model->find($id))
return self::getUniqueId($model);
return $id;
}
}
?>
Model (app/Models/User.php)
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
public static function boot(){
parent::boot();
$creationCallback = function ($model) {
if (empty($model->{$model->getKeyName()}))
$model->{$model->getKeyName()} = Constants::getUniqueId(new self());
};
static::creating($creationCallback);
}
}
?>
Explanation: Instead of calling the getUniqueId method in every controller. You can write it inside model.
From the #Joel Hinz answer :
public function set_number() {
$number = mt_rand(1000000000, 9999999999);
return User::where('number', $number)->exists() ? $this->set_number() : $number;
}
Hope that helped.

Categories