Query Inside a redis model in yii2 - activerecord

I have created a redis model, which should store statistics like this:
<?php
namespace app\models;
use Yii;
use yii\base\Model;
use \yii\redis\ActiveRecord;
use \yii\redis\ActiveQuery;
class StatsModel extends ActiveRecord
{
public function attributes()
{
return ['id', 'hits', 'user', 'ua', 'ip','os'];
}
public function rules()
{
return [
['user', 'required'],
['user','string'],
['ip', 'required'],
['ip', 'integer'],
['hits', 'integer'],
['ua','string'],
['os','integer']
];
}
public static function total_user_hits($username)
{
$query = new ActiveQuery($this);
$query->find()->where('user = '.$user)->all();
}
public static function getDb()
{
return \Yii::$app->db_redis;
}
}
Now, i'm trying to make a static function, which i can use, to count all thi hits value for specific user in redis. I'm creating an $query = new ActiveQuery($this); each time in the function, but can how can initiliase just one copy of the query to always use it? If i do it like class property:
public $query = new ActiveQuery($this);
I get error expression is not allowed as field default value

You should not reuse existing query object (unless you want to make query with the same conditions) - ActiveQuery is mutable, it means that previous queries may change its state:
$query = new ActiveQuery(StatsModel::class);
$result1 = $query->andWhere('user = 1')->all(); // 1 result
$result2 = $query->andWhere('user = 2')->all(); // no results
Second query will not return anything, since it will create condition like WHERE user = 1 AND user = 2 which is always false.
If you're afraid about performance, you should not. Creating ActiveQuery object has negligible overhead. Creating objects in PHP is relatively cheap and ActiveQuery is quite lightweight - the most time consuming thing will be actual query to redis/db.

Related

How to programmatically get relationship data without having a model instance?

I have the following working code that gives me a collection of a model type that each have any of the given relationship values (like a tag with id 1, 2 or 3):
<?php
public function getEntitiesWithRelationValues($entityType, $relations = []) {
$related = new EloquentCollection();
$locale = App::getLocale();
$entityType = new $entityType(); // bad?
// $entityType = new ReflectionClass($entityType); // not working
foreach ($relations as $relation => $modelKeys) {
if ($entityType->{$relation}()->exists()) {
$relatedClass = get_class($entityType->{$relation}()->getRelated());
$relationPrimaryKeyName = ($instance = new $relatedClass)->getQualifiedKeyName();
$relationEntities = $entityType::where('published->' . $locale, true)
->whereHas($relation, function (Builder $query) use($modelKeys, $relationPrimaryKeyName) {
$query->whereIn($relationPrimaryKeyName, $modelKeys);
})
->get()
->sortKeysDesc()
->take(10)
;
$related = $related->concat($relationEntities->except($related->modelKeys()));
}
}
return $related;
}
I feel $entityType = new $entityType(); is bad code because I dont want to create a new model. The reflection class throws the error "ReflectionClass undefined method {$relation}". How can I get the relationship data of a model type without actually loading/ instantiating a model?
A few weeks ago I asked something similar here but in that case I did have a model loaded.
You could consider the following solution:
Actually use a Model instance as the input to your getEntitiesWithRelationValues() function, since one way or another you are going to retrieve the relationships for a specific instance right?
Create a property for your model:
public static $relationNames = ['tags', 'posts', 'comments'];
Retrieve that property in your code using MyModel::$relationNames and call whichever functions you need.
Alternative solution using reflection
The snippet below uses reflection to find all public methods of User::class, then filter return types based on if it is a subclass of \Illuminate\Database\Eloquent\Relations\Relation. Note that you need to annotate the return types specifically using the new language struct like public function myMethod() : MyReturnType { }.
use App\Models\User;
use Illuminate\Database\Eloquent\Relations\Relation;
$relationshipMethods = collect(
(new ReflectionClass(User::class))
->getMethods(\ReflectionMethod::IS_PUBLIC)
)
->filter(function(\ReflectionMethod $method) {
// The `?` makes the code work for methods without an explicit return type.
return is_subclass_of($method->getReturnType()?->getName(), Relation::class);
})->values();
// $relationshipMethods now is a collection with \ReflectionMethod objects.

How to add rows with unique column to the database?

I have a table with unique column, it consists of 8 characters of random English letters and numbers.
I need to be able to seed the table by adding more data to the existing data.
I am supposed to enter a given amount of rows.
I tried using Factories and Seeders but then realized it's not working as expected:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Code extends Model
{
protected $table = 'codes';
}
protected $fillable = [
'code',
'is_valid'
];
Factory:
public function definition()
{
return [
'code' => $this->faker->regexify('[A-Z0-9]{8}')
];
}
Then I am supposed to add data by running:
$codes = Code::factory()->count(10000)->create();
and after that, if I run it multiple times I might end up with constraint violation:
// add some more data
$codes = Code::factory()->count(10000)->create();
$codes = Code::factory()->count(10000)->create();
What is an efficient way to be able to add given amount of rows and make sure they're unique?
All other questions I've seen here are similar, but not working for my case.
Change in your factory definition this line from this: 'code' => $this->faker->regexify('[A-Z0-9]{8}') to: $faker->unique()->regexify('[A-Z0-9]{8}');.
In your factory you can use the unique() faker modifier : https://fakerphp.github.io/#modifiers
public function definition()
{
return [
'code' => $this->faker->unique()->regexify('[A-Z0-9]{8}')
];
}
Edit: when running the factory with existing data, you can check if the faked data already exists in the database like:
public function definition()
{
do {
$fakeCode = $this->faker->unique()->regexify('[A-Z0-9]{8}');
} while (DB::table('codes')->where('code', $fakeCode)->exists());
return [
'code' => $fakeCode
];
}

Eloquent's First or Create does not consider the mutator before checking if the record exists?

Imagine that I have the following model:
class Link extends Model
{
protected $fillable = ['path', 'secret'];
public function setSecretAttribute($value)
{
$this->attributes['secret'] = sha1($value);
}
}
And the following code:
$link = Link::firstOrCreate(['path' => $someValue, 'secret' => $anotherValue]);
Is there something wrong with my code or "firstOrCreate" ignore the mutator when it will check if the registry already exists? If it ignores, I just have to add sha1 encryption to "anotherValue" to get the behavior I expect. But my question is, wouldn't that be redundant?
Not a lot of detail in this question, but if path is a unique identifier this is easily done with the second argument to Model::firstOrCreate().
If the model can not be found in the database, a record will be inserted with the attributes resulting from merging the first array argument with the optional second array argument.
So your code would look like this:
$link = Link::firstOrCreate(
['path' => $someValue],
['secret' => $anotherValue]
);
If both values are required to uniquely identify the record, you can simply do a conditional check:
$link = Link::where('path', $someValue)
->where('secret', sha1($anotherValue))
->first();
if (!$link) {
Link::create(['path' => $someValue, 'secret' => $anotherValue]);
}
Note that mutators apply only to database writes done via Eloquent methods. When you are reading the database, you must manually hash the value, as I've done.
To avoid manually hashing the value, the best option would be a query scope on the model like so:
class Link extends Model
{
protected $fillable = ['path', 'secret'];
public function setSecretAttribute($value)
{
$this->attributes['secret'] = sha1($value);
}
public function scopeSecretHashOf($query, $secret)
{
return $query->where('secret', '=', sha1($secret));
}
}
Which you could then use like this:
$link = Link::where('path', $someValue)
->whereSecretHashOf($anotherValue)
->first();
if (!$link) {
Link::create(['path' => $someValue, 'secret' => $anotherValue]);
}
In theory you could intercept and rewrite the query with a custom grammar but it would likely be more effort than it's worth.

Eager load for collections on the fly

I have this, user has many to many property via pivot property_users.
I am making somehow reusable classes in my webapp.
These are the models with their eager loading functions:
//User model
public function properties()
{
return $this->belongsToMany(Property::class, 'property_users', 'user_id', 'property_id');
}
//Property model
public function property_users()
{
return $this->hasMany(PropertyUser::class, 'property_id', 'id');
}
//PropertyUser model
public function user()
{
return $this->belongsTo(User::class);
}
//GetProperties class
public function handle()
{
return auth()->user()->properties()->get();
}
//somewhere in a feature
$properties = $this->run(GetProperties::class);
//this returns valid properties under the logged in user
I now need to get the chat_username in property_users that belongs to this user
I manage to make it work if I loop through the properties and then doing it on the fly.
$properties = $properties->map(function($property) {
$propertyUsers = $property->property_users()->get();
$chatUsername = null;
foreach($propertyUsers as $propertyUser) {
if($propertyUser->property_id == $property->id) {
$chatUsername = $propertyUser->chat_username;
}
}
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $chatUsername
];
});
But I am trying to reduce query on loop to reduce hits especially when they are on multiple properties in the database.
The other way is that I can add the property_users in the eager loading under GetProperties class by updating it to:
$query = Property::query();
$query->with(['property_users']);
$query->whereHas('property_users', function($qry) {
$qry->where('user_id', Auth::user()->id);
});
$properties = $query->get();
return $properties;
But I do not want to rely on adding more eager loading to the original GetProperties class as the GetProperties will get fat and I do not really need those data (let's say adding property_invoices, property_schedules, etc but not really needing it in some area).
Rather, I want to do the eager loading on the fly but with a twist! This is how I would imagine it:
Collect all the ids from the properties, do the fetch using wherein and apply all the users to the properties in a single query. This way it will be even more beautiful.
Maybe something like this: (using the original GetProperties class)
$properties = $this->run(GetProperties::class);
//this does not work. The error is: Method Illuminate\Database\Eloquent\Collection::property_users does not exist.
$property->property_users = $properties->property_users()->get();
Would be great if someone can show me how to do it.
What about eager loading only the fields you actually need?
$query->with('property_users:id,user_id');
The model will not get fat, and you will not need to do separate queries in the loop.
It is documented in the official documentation: https://laravel.com/docs/5.8/eloquent-relationships#eager-loading , see Eager Loading Specific Columns
Edit: if you want to perform the query after the GetProperties class, you will need to collect all the ids and perform a second query. I honestly don't like this second approach, because it is far slower, less performant and I consider it less elegant then adding a single line in the GetProperties class, but it is gonna work:
$properties = $this->run(GetProperties::class);
$ids = $properties->pluck('id'); // Get all the ids as an array
$propertyUsers = propertyUsers::whereIn('property_id', $ids)->get(); // Get the propertyUsers model
foreach($properties as $property) {
$property->property_users = $propertyUsers->where('property_id', $property->id); // Not even sure you can do that, proerty_users may not been writable
}
Alright, after reading here and there.
I found this article: https://laravel-news.com/eloquent-eager-loading
The solution is really nice. Simply:
$properties = $this->run(GetProperties::class);
//lazy load
$properties->load('property_users');
$properties = $properties->map(function($property) {
$user = $property->property_users->first(function($user) {
if($user->user_id == Auth::user()->id) {
return $user;
}
})->only('chat_username');
return [
'name' => $property->name,
'id' => $property->id,
'chat_username' => $user['chat_username']
];
});
After checking query logs:
//auth query done by middleware
[2019-05-21 07:59:11] local.INFO: select * from `users` where `auth_token` = ? or `temporary_auth_token` = ? limit 1 ["token_removed_for_security_purpose","token_removed_for_security_purpose"]
//These are the queries made:
[2019-05-21 07:59:11] local.INFO: select `properties`.*, `property_users`.`user_id` as `pivot_user_id`, `property_users`.`property_id` as `pivot_property_id` from `properties` inner join `property_users` on `properties`.`id` = `property_users`.`property_id` where `property_users`.`user_id` = ? [8]
[2019-05-21 07:59:11] local.INFO: select * from `property_users` where `property_users`.`property_id` in (2, 4)
This way, I can keep my GetProperties as small as possible, then just lazy load it whereever I need it.

Paginate with Eloquent but without instantiation Models

We have two Models:
SimpleModel (id, country, code)
ComplexRelatedModel (id, name, address)
SimpleModel has many ComplexRelatedModel, then
class Product extends Model
{
protected $fillable = [
'name'
];
/* hasOne */
public function complexRelatedChild()
{
return $this->hasOne(self::class, 'parent_id', 'id');
}
}
If we do
$simples = SimpleModel
->with('complexRelatedChild')
->simplePaginate(100000 /* a lot! */);
And we need only do
foreach ($simples as $simple) {
echo $simple->complexRelatedChild->name;
}
Any ComplexChild has hydratated and ready. This takes a lot of memory in my case. And we need just one field without any funciton or feature of Model.
It's possible use some data field from related object or with eloquent this isn't possible?
Not sure I completely understand your question. You want to only load one field from the complexRelatedChild relation to keep memory limit down?
You could do:
$simples = SimpleModel::with(['complexRelatedChild' => function($query){
return $query->select(['id', 'name']);
})
->simplePaginate(100000);
Which can be simplified to:
$simples = SimpleModel::with('complexRelatedChild:id,name')
->simplePaginate(100000);
However if I were you, I would try to paginate less items than 100000.
Update:
You could use chunk or cursor functions to process small batches of SimpleModel and keep memory limit down.
SimpleModel::chunk(200, function ($simples) {
foreach ($simples as $simple) {
}
});
or
foreach (SimpleModel::cursor() as $simple) {
}
See the documentation for more information

Resources