Optimizing code with chunk or cursor in laravel - laravel

I'm having Company Model and Contact Model defined in my Laravel 5.4 application, both of them have many to many relationship. So, for example contacts model has:
public function company()
{
return $this
->belongsToMany('App\Company', 'company_contact','contact_id', 'company_id')->withTimestamps();
}
Now I've a data set where I want to pull all contacts data and there company details so I was using:
public function getData()
{
$contacts = Contact::all();
foreach($contacts as $contact)
{
$getCompany = $contact->company()->withPivot('created_at')->orderBy('pivot_created_at', 'desc')->first();
$getCompany->contacts = Company::find($getCompany->id)->contacts;
$contact->company = $getCompany;
$contact->companies_interested = json_decode($contact->companies_interested);
$companies = [];
if($contact->companies_interested)
{
foreach($contact->companies_interested as $companiesInterested)
{
$getCompany = Company::withTrashed()->find($companiesInterested);
$companies[] = array(
'value' => $getCompany->id,
'label' => $getCompany->name
);
}
$contact->companies_interested = json_encode($companies);
}
}
return response()->json([
'model' => $contacts
], 200);
}
This works perfectly fine for small data set, but while using large number of data it fails (approx 10,000 fields), I guess php memory fails to load when it comes to large data set. I was going through Laravel docs to find out the solution and came to know about chunk() and cursor() methods, Can someone guide me what can be done in this problem or what can be the approach to overcome this.
Thanks

I recommend you to test both methods for some quirkiness of your system.
Chunk:
It will "paginate" your query, this way you use less memory.
Uses less memory
It takes longer
`
public function getData() {
Contact::chunk(1000, function ($contacts) {
foreach ($contacts as $contact) {
//rest of your code...
}
});
}
`
Cursor:
You will use PHP Generators to search your query items one by one.
It takes less time
Uses more memory
`
public function getData() {
foreach (Contact::cursor() as $contact) {
//rest of your code...
}
}
`
For a more detailed explanation see this answer: What is the difference between laravel cursor and laravel chunk method?
For performance testing see this post: https://translate.google.com/translate?hl=en&sl=auto&tl=en&u=http%3A%2F%2Fqiita.com%2Fryo511%2Fitems%2Febcd1c1b2ad5addc5c9d

Related

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.

Filtering one-to-many connections in entity

I have an entity User which has a one-to-many connection to questions. Questions has a one to many connection to answers. The question and answer entity each has a property called state.
In generally after execute a GET request API Platform returns all users, with all questions including all answers. That works fine!
Now I would like to implement a get request that returns the user with all questions that has a specific state (e.g. "X"). The questions should only include the answers with the same state ("X").
I used the filter function (to filter the whole not necessary data)
Therefore I generated a controller called GetUserObjectAction which the following function
public function __invoke(PaginatorInterface $data, Request $request): PaginatorInterface
{
$repo = $this->managerRegistry->getRepository(Question::class);
foreach ($data as $value) {
$q = $value->getQuestions()->filter(function($q1) {
if($q1->getState() === 'a') {
$q1->values = $q1->values->filter(function($a) {
return $a->getState() === 'a';
});
return true;
} else {
return false;
}
return ;
});
$value->setQuestions($int);
}
return $data;
}
Is there a better way to implement it?
Thanks
You chose the right way because you did not tell us how are your data managed (Doctrine ORM/ODM, custom data providers) we cannot tell you more. But I suggest you are using API Platform defaults, so you can filter your data before fetching them in your QuestionRepository and omit to iterate over data.
Here is example:
QuestionRepository
...
public function findWithAnswersByState(string $state): array
{
$qb = $this->createQueryBuilder('q')
->join('q.answers', 'a')
->andWhere('q.state = :state')
->andWhere('a.state = :state')
->setParameter('state', $state);
return $qb->getQuery()->getResult();
}
...
Controller:
...
public function __invoke(PaginatorInterface $data, Request $request): PaginatorInterface
{
$repo = $this->managerRegistry->getRepository(Question::class);
return $repo->findWithAnswersByState('a');
}
...

Adding methods to Eloquent Model in Laravel

I'm a bit confused how I am to add methods to Eloquent models. Here is the code in my controller:
public function show($id)
{
$limit = Input::get('limit', false);
try {
if ($this->isExpand('posts')) {
$user = User::with(['posts' => function($query) {
$query->active()->ordered();
}])->findByIdOrUsernameOrFail($id);
} else {
$user = User::findByIdOrUsernameOrFail($id);
}
$userTransformed = $this->userTransformer->transform($user);
} catch (ModelNotFoundException $e) {
return $this->respondNotFound('User does not exist');
}
return $this->respond([
'item' => $userTransformed
]);
}
And the code in the User model:
public static function findByIdOrUsernameOrFail($id, $columns = array('*')) {
if (is_int($id)) return static::findOrFail($id, $columns);
if ( ! is_null($user = static::whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
So essentially I'm trying to allow the user to be retrieved by either user_id or username. I want to preserve the power of findOrFail() by creating my own method which checks the $id for an int or string.
When I am retrieving the User alone, it works with no problem. When I expand the posts then I get the error:
Call to undefined method
Illuminate\Database\Query\Builder::findByIdOrUsernameOrFail()
I'm not sure how I would go about approaching this problem.
You are trying to call your method in a static and a non-static context, which won't work. To accomplish what you want without duplicating code, you can make use of Query Scopes.
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
if ( ! is_null($user = $query->whereUsername($id)->first($columns))) {
return $user;
}
throw new ModelNotFoundException;
}
You can use it exactly in the way you are trying to now.
Also, you can use firstOrFail:
public function scopeFindByIdOrUsernameOrFail($query, $id, $columns = array('*')) {
if (is_int($id)) return $query->findOrFail($id, $columns);
return $query->whereUsername($id)->firstOrFail($columns);
}
Your method is fine, but you're trying to use it in two conflicting ways. The one that works as you intended is the one in the else clause, like you realised.
The reason the first mention doesn't work is because of two things:
You wrote the method as a static method, meaning that you don't call it on an instantiated object. In other words: User::someStaticMethod() works, but $user->someStaticMethod() doesn't.
The code User::with(...) returns an Eloquent query Builder object. This object can't call your static method.
Unfortunately, you'll either have to duplicate the functionality or circumvent it someway. Personally, I'd probably create a user repository with a non-static method to chain from. Another option is to create a static method on the User model that starts the chaining and calls the static method from there.
Edit: Lukas's suggestion of using a scope is of course by far the best option. I did not consider that it would work in this situation.

Problems updating using Eloquent

Right now I'm working on my first update function using Eloquent ORM. Trying to follow the docs, I have this in my model:
public function updateAvailability()
{
$this->active = Input::get('available');
$this->activeDetails = Input::get('availableStatus');
$this->save();
}
which returns:
Relationship method must return an object of type Illuminate\Database\Eloquent\Relations\Relation
and all of this is being called to in my controller as:
public function updateProfile($id)
{
if(Input::get('type')=='availability'){
$availability = User::find($id)->updateAvailability;
}
$name = str_replace(' ', '', Input::get('name'));
return Redirect::to('people/'.$name);
}
Are there some gaps in my understanding of updating in Eloquent? (I'm sure there are). I would love to use ajax to handle it, but I can't seem to find the right resources to get that working.
SOLVED: $availability = User::find($id)->updateAvailability; needed to be changed to $availability = User::find($id)->updateAvailability();. That's all.

CodeIgniter view problem

This is my first day playing with CI and I really like it so far but have a problem I can't solve on my own. The issue is that I need to generate single view with two controller functions. One div should include selected row by ID from table A and other div should loop foreach on array from table B.
public function index()//div A
{
$data['query'] = $this->db->get_where('beer', array('id' => 1));
$this->load->view('corp/corp_view', $data);
}
public function loadList() //div B
{
$data['q'] = $this->db->get_where('list', array('id' => 1));
$this->load->view('corp/mentor_list_view', $data);
}
I tried to solve this for few hours by creating another view for loadList() and then including it in the the main view like "$this->load->view()" but I'm getting the values from the index() function query table 'beer' not 'list' table as intended. Again I'm new to this and would appreciate your help.
Thank you for your help.
Thanks for the extra info, i can help yah out now.
In Codeigniter if you want to make a function that is not able to be called by the user just precede it with a '_'. So in your case:
public function index()//div A
{
$data['query'] = $this->db->get_where('beer', array('id' => 1));
$data['query2'] = $this->_mySecondQuery();
$this->load->view('corp/corp_view', $data);
}
public function _mySecondQuery() //div B
{
return $this->db->get_where('list', array('id' => 1));
}
Now in the index page you have access to both queries. Btw, I would not suggest doing much DB work in the controller. DB work is meant to be done in Models. For more info on those see: Codeigniter Models

Resources