Yii2 ActiveRecord cache - caching

How to use ActiveRecotd cache for Yii 2? I did't find any examples in official docs. In Google I found 2 examples, first is:
$db = self::getDb();
$object = $db->cache(function ($db) use($id) {
return self::findOne($id);
});
But it doesn't work for Model, I tested with updated framework. Other example is:
$data = \Yii::$app->cache->get('some_var_' . $id);
if ($data === false)
{
$data = self::findOne($id);
\Yii::$app->cache->set('some_var_' . $id, $data, 60);
}
It's working fine, but it's not ActiveRecord caching it's data caching, So we haven't got ActiveRecord caching in Yii 2?

1) Use cache like that:
$db = Yii::$app->db;// or Category::getDb()
$result = $db->cache(function ($db) use ($id) {
return Category::find()->where(['id' => $id])->all();
}, CACHE_TIMEOUT);
2) If you may use query dependency, use like that:
$db = Yii::$app->db;// or Category::getDb()
$dep = new DbDependency();
$dep->sql = 'SELECT count(*) FROM category';
$result = $db->cache(function ($db) use ($id) {
return Category::find()->where(['id' => $id])->all();
}, CACHE_TIMEOUT, $dep);

I too am having trouble with this. Here's my workaround for the time being for a hasOne() relationship.
public function getGroup()
{
if(isset(static::$_getGroup[$this->id])) {
return static::$_getGroup[$this->id];
}
$Group = $this->hasOne(BillChargesGroup::className(), ['id' => 'group_id'])->one();
static::$_getGroup[$this->id] = $Group;
return $Group;
}
I only want to cache data for the current request, so this works. However because I'm using ->one(); it does not return the ActiveQuery object if we call $model->getGroup() (which I found is good for extending queries)
Unfortunately if I do return the ActiveQuery object, Yii2 does some "magic" on it and always does a SELECT * which I can't control.

Since 2.0.14 you can use the following shortcuts:
(new Query())->cache(7200)->all();
// and
User::find()->cache(7200)->all();
Source: https://www.yiiframework.com/doc/guide/2.0/en/caching-data

Related

Can we use Local Scopes in Symfony?

I wondering about a subject. We can use Local scopes in Laravel but i don't know if for Symfony.
Doc : Laravel Local Scopes
Well, my question is can i use it in Symfony? Is this possible ?
Have a good day
You can do the same with classical methods in your repository.
I can show you an example(using source code from the docs):
public function findAllGreaterThanPrice(int $price, bool $includeUnavailableProducts = false): array
{
// automatically knows to select Products
// the "p" is an alias you'll use in the rest of the query
$qb = $this->createQueryBuilder('p')
->where('p.price > :price')
->setParameter('price', $price)
->orderBy('p.price', 'ASC');
if (!$includeUnavailableProducts) {
$qb->andWhere('p.available = TRUE');
}
$query = $qb->getQuery();
return $query->execute();
// to get just one result:
// $product = $query->setMaxResults(1)->getOneOrNullResult();
}
Here instead of return "$query->execute()", you can return $qb and chain methods will available.
You can do something like that :
$repo->findAllActive()->findAllGreaterThan12();
Here $repo would be the repository injected in your controller.
In both method, you would have just a where and a return of querybuilder.

Laravel: Cache with pagination clear issue

I have laravel (7.x) application. I recently added the cache functionality for the performance boost. After implementing the cache functionality, I was having trouble with the pagination while loading the data in grid format, so I googled for the solution and found this Pagination with cache in Laravel.
Although, it did solve my problem. But, the case is that I have about 100 pages and due to the solution I found, each page has it's own cache. Now, if I create or update any record then it doesn't reflect in the grid because the data is loaded from the cache.
PostController.php:
...
$arraySearch = request()->all();
# calculating selected tab
$cache = (!empty(request()->inactive)) ? 'inactive' : 'active';
$cacheKey = strtoupper("{$this->controller}-index-{$cache}-{$arraySearch['page']}");
# caching the fetch data
$arrayModels = cache()->remember($cacheKey, 1440, function() use ($arraySearch) {
# models
$Post = new Post();
# returning
return [
'active' => $Post->_index(1, 'active', $arraySearch),
'inactive' => $Post->_index(0, 'inactive', $arraySearch),
];
});
...
Post.php:
public function _index($status = 1, $page = null, $arraySearch = null)
{
...
$Self = self::where('status', $status)
->orderBy('status', 'ASC')
->orderBy('title', 'ASC')
->paginate(10);
...
return $Self;
}
How do I clear all this cache to show the newly created or updated record to with the updated values.?
1. Store All pages under the same tag:
As seen on the documentation: https://laravel.com/docs/master/cache#storing-tagged-cache-items
You can use tags to group cached items.
$cacheTag = strtoupper("{$this->controller}-index-{$cache}");
$arrayModels = cache()->tags([$cacheTag])->remember($cacheKey, 1440, function() use ($arraySearch) {
...
2. Set an event listener on Post to clear the tag
You can run an Event listener on your Post update() or create() events.
https://laravel.com/docs/7.x/eloquent#events-using-closures
You can then clear the tag cache using
Cache::tags([$cacheTag])->flush();
I know this isn't the proper solution. But, until I find the proper way to do it, this is the option I am kind of stuck with.
PostController.php:
public function index()
{
...
$arraySearch = request()->all();
# calculating selected tab
$cache = (!empty(request()->inactive)) ? 'inactive' : 'active';
$cacheKey = strtoupper("{$this->controller}-index-{$cache}-{$arraySearch['page']}");
# caching the fetch data
$arrayModels = cache()->remember($cacheKey, 1440, function() use ($arraySearch) {
# models
$Post = new Post();
# returning
return [
'active' => $Post->_index(1, 'active', $arraySearch),
'inactive' => $Post->_index(0, 'inactive', $arraySearch),
];
});
...
}
public function store()
{
...
Artisan::call('cache:clear');
...
}
I'll post the proper solution when I find one. Till then I am using this one.
There is a method in Laravel Model class called booted (not boot, which is having a different purpose). This method runs every time something is "saved" (including "updated") or "deleted".
I have used this as following (in a Model; or a Trait, included in a Model):
protected static function booted(): void
{
$item = resolve(self::class);
static::saved(function () use ($item) {
$item->updateCaches();
});
static::deleted(function () use ($item) {
$item->updateCaches();
});
}
"updateCaches" is a method in the Trait (or in the Model), that can have the code to update the cache.

What's the solution for adding unique slug to prevent duplicate entry in creating?

i'm working on a project for my homework which i'm trying to make it work as a spa, but I've got some problem in making slug for posts.
in the tutorial which i've followed, instructor used this to make a slug from title :
protected static function boot()
{
parent::boot();
static::creating(function ($course){
$course->slug = str_slug($course->name);
});
}
now, if i make this table unique, which this is what i wanna do. how should i prevent app from giving me duplicate entry? or how can i add something to slug, like a number, Every time i get duplicate entry?
if i make a post with This Post name twice, second time, i get duplicate Error.
In my opinion your selected answer is not getting close to something efficient. In large applications the 2 random strings can be overwritten in a short time and then you will have huge issues (code and DB).
A safer approach is to build a service and use that when you save the slug in the DB. Or course this is not 100% perfect but definitely is better then to increment 2 random strings. That, by the way, can also affect the SEO part of the app.
Below you can find my example:
The Model
public static function boot()
{
parent::boot();
static::saving(function ($model) {
$slug = new Slug();
$model->slug = $slug->createSlug($model->title);
});
}
The Service
<?php
namespace App\Services;
use App\Job;
class Slug
{
/**
* #param $title
* #param int $id
* #return string
* #throws \Exception
*/
public function createSlug($title, $id = 0)
{
// Normalize the title
$slug = str_slug($title);
// Get any that could possibly be related.
// This cuts the queries down by doing it once.
$allSlugs = $this->getRelatedSlugs($slug, $id);
// If we haven't used it before then we are all good.
if (!$allSlugs->contains('slug', $slug)) {
return $slug;
}
// Just append numbers like a savage until we find not used.
for ($i = 1; $i <= 100; $i++) {
$newSlug = $slug . '-' . $i;
if (!$allSlugs->contains('slug', $newSlug)) {
return $newSlug;
}
}
throw new \Exception('Can not create a unique slug');
}
protected function getRelatedSlugs($slug, $id = 0)
{
return Model::select('slug')->where('slug', 'like', $slug . '%')
->where('id', '<>', $id)
->get();
}
}
You could use inbuilt Str class, and create some random strings in your Post slug. Example:
static::creating(function ($course){
$course->slug = str_slug($course->name . Str::random( 2 ));
});
This will add 2 random strings on each slug you create, which will ensure there are no duplicates. You can find more about Str class here.
you can use laravel Inbuilt Helper, follow below link.
https://laravel.com/docs/5.8/helpers#method-str-slug
$data = 'My Data'
$slug = Str::slug($data, '-');
dd($slug);
add this in your controller
use Illuminate\Support\Str;

Fetching a cached Eloquent collection of 10 items times out

I'm building a search functionality that returns large collections which are paginated using a LengthAwarePaginator. I'm trying to cache results using a key called $searchFilter_$query_$offsetPages for a single page of returned results (10 items). It goes into the cache just fine. However, it times out when I try to check using Cache::has($key) or fetch using Cache::get($key).
The same problem occurs in the browser as well as in artisan Tinker. Strangely, when I put a random set of 10 items into the cache in Tinker and fetch them back, everything works fine. I'm using Redis as the cache driver.
Here is my controller method:
public function search($filter, $query, $layout, Request $request) {
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = $filter . "_" . $query . "_" . $offsetPages;
if(Cache::has($cacheKey)) {
\Log::info("fetching results from cache");
$data = Cache::get($cacheKey);
$totalCt = $data[0];
$results = $data[1];
} else {
$results = $this->getResults($filter, $query);
$totalCt = $results->count();
$results = $results->slice($offsetPages, $this->resultsPerPage);
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
}
$results = new LengthAwarePaginator($results,
$totalCt,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(['filter' => $filter, 'query' => $query, 'layout' => $layout, 'results' => $results]);
}
}
So, the issue was that many of the models in the collection returned from my getResults() method were obtained via relationship queries. When I would dd($results) on the single page of 10 results, I could see that there was a "relations" field on each model. Inside that array were thousands of recursively related models based on the relationship I originally queried. I was unable to find any information about an option to not eager load these related models. Instead I came up with a bit of a hacky workaround to fetch the models directly:
$results = $results->slice($offsetPages, $this->resultsPerPage);
//load models directly so they don't include related models.
$temp = new \Illuminate\Database\Eloquent\Collection;
foreach($results as $result) {
if(get_class($result) == "App\Doctor") {
$result = Doctor::find($result->id);
} else if(get_class($result == "App\Organization")) {
$result = Organization::find($result->id);
}
$temp->push($result);
}
$results = $temp;
\Log::info("caching results");
Cache::put($cacheKey, [$totalCt, $results], 5);
If anyone knows the best practice in this situation, please let me know. Thanks!
Edit:
I've found a better solution instead of the above workaround. If I query my relationships like this: $taxonomy->doctors()->get() rather than $taxonomy->doctors, it does not load in the huge recusive relations.
I dont really see why your code doesn't work. The only potential problems I see are the cache keys, which could contain problematic characters, as well as the way you check for a cached value. As you are using Cache::has($key) before Cache::get($key), you could end up with a race condition where the first call returns true and the latter null because the cached value timed out just between the two calls.
I tried to address both issues in the following snippet:
public function search($filter, $query, $layout, Request $request)
{
if($layout == "list-map") {
return view("list-map")->with(['filter' => $filter, 'query' => $query, 'layout' => 'list-map']);
} else {
$offsetPages = $request->input('page', 1) - 1;
$cacheKey = md5("{$filter}_{$query}_{$offsetPages}");
$duration = 5; // todo: make this configurable or a constant
[$totalCount, $results] = Cache::remember($cacheKey, $duration, function () use ($filter, $query) {
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
return [$totalCount, $filteredResults];
});
$results = new LengthAwarePaginator($results,
$totalCount,
$this->resultsPerPage,
$request->input('page', 1),
['path' => LengthAwarePaginator::resolveCurrentPath()]
);
return view($layout)->with(compact('filter', 'query', 'layout', 'results'));
}
}
The inbuilt function Cache::remember() doesn't use Cache::has() under the hood. Instead, it will simply call Cache::get(). As this function will return null as default if no cache was hit, the function can easily determine if it has to execute the closure or not.
I also wrapped the $cacheKey in md5(), which gives a consistently valid key.
Looking at the following part of your code
$results = $this->getResults($filter, $query);
$totalCount = $results->count();
$filteredResults = $results->slice($offsetPages, $this->resultsPerPage);
I am quite sure the whole search could be improved (independently of the caching). Because it seems you are loading all results for a specific search into memory, even if you throw away most parts of it. There is certainly a better way to do this.

Laravel Excel - Select columns to export

I'm using Laravel Excel from maatwebsite but i have a problem with export data to xls file. I have this query but i don't need to show all columns in the xls file but i need to select all this columns to do operations before download the file. In my select i have 8 columns but in my headers i just have 7 to show but this doesn't work because the 8ª column appears too.
MY FUNCTION:
public function toExcel($id) { $b = Budget::find($id);
$budget = Budget_Item::join('budgets', 'budget__items.id_budget', '=', 'budgets.id')
->join('items_sizes', 'budget__items.id_itemSize', '=', 'items_sizes.id')
->join('items', 'items_sizes.id_item', '=', 'items.id')
->join('material_types', 'items_sizes.id_materialType', '=', 'material_types.id')
->select('items.reference AS Referência', 'items.name AS Descrição', 'items_sizes.size AS Tamanho', 'material_types.material_type AS Material', 'budget__items.amount AS Quantidade', 'items_sizes.unit_price AS Val.Unitário', 'budget__items.price AS Val.Total', 'budget__items.purchasePrice')
->where('id_budget', '=', $id)
->get();
$budgetUpdate = [];
$budgetUpdate[] = ['Referência', 'Descrição', 'Tamanho', 'Material', 'Quantidade', 'Val.Unitário', 'Val.Total'];
foreach ($budget as $key)
{
if ($key->purchasePrice > 0)
{
$key->unit_price = $key->purchasePrice;
}
$budgetUpdated[] = $key->toArray();
}
Excel::create('Proposta_'.$b->reference, function($excel) use($budgetUpdated)
{
// Set the title
$excel->setTitle('Proposta');
$excel->sheet('Sheetname', function($sheet) use($budgetUpdated)
{
$sheet->fromArray($budgetUpdated, null, 'A1', false, true);
});
})->download('xls');}
How can i solve that?
Thank's
Tested on Laravel excel 3.1 docs
on controller
public function exportAsCsv()
{
return Excel::download(new MyExport, 'invoices.xlsx');
}
on MyExport, look at the map function
class MyExport implements FromCollection, WithHeadings, WithMapping{
public function collection(){
return MyTable:all();
}
// here you select the row that you want in the file
public function map($row): array{
$fields = [
$row->myfield2,
$row->myfield1,
];
return fields;
}
}
also check this
For what its worth I ran into a similar issue and didnt find much in terms of a fix so heres my solution.
1. I query the DB in the callback and get all the data I need.
2. I then go over the collection and create a new array on each iteration and assign a value & header. Works great for picking out columns from a model
Excel::create('User List Export', function($excel) {
$excel->sheet('Users', function($sheet) {
$email = yourModelGoesHere::all();
foreach ($email as $key => $value) {
$payload[] = array('email' => $value['email'], 'name' => $value['name']);
}
$sheet->fromArray($payload);
});
})->download('xls');
You can try this way.
$user_id = Auth::user()->id
Excel::create('User', function($excel) use ($user_id){
$excel->sheet('Sheet', function($sheet) use($user_id){
$user = User::where('user_id', $user_id)->select('name', 'email', 'role', 'address')->get();
});
})->export('xls');
return redirect('your route');

Resources