Laravel - Shared data reduce number of queries - php

I am trying to make a global search feature on my website, and for that I need to query the database to get the records. The frontend of my application expect the following array:
[
['name' => 'Result #1...', 'url' => 'http://...'],
['name' => 'Result #2...', 'url' => 'http://...'],
['name' => 'Result #3...', 'url' => 'http://...'],
]
To accomplish this, I have added the below in my AppServiceProvider:
public function boot()
{
view()->composer('*', function($view) use ($auth) {
return \View::share('SearchData', (new GlobalSearch($auth->user()->currentTeam))->all());
});
}
The $SearchData is created in the GlobalSearch class:
class GlobalSearch
{
public $data;
public function __construct(public Team $team){
$this->data = $team->properties()->with(['leases', 'leases.tenant', 'leases.files', 'leases.invoices']);
}
protected function propertyData() : array
{
$properties = $this->data->get();
return $properties->map(function ($property) {
$array['name'] = \Str::limit($property->address, 40);
$array['url'] = route('properties.show', ['property' => $property]);
return $array;
})->toArray();
}
public function all() : array
{
return $this->propertyData();
}
}
Now the above code does work - I successfully get an array in the correct mapping. However, in my database I only have 1 property in the properties table - yet, there are being executed 90 duplicate queries for a single page load.
Why is this happening? I can't seem to locate why these queries are being duplicated

You can actually remove the view()->composer('*', function) part. Since View::share() does the exact same.
$searchData = (new GlobalSearch($auth->user()->currentTeam))->all();
View::share('SearchData', $searchData);
Optionally:
You could bind the GlobalSearch class to the container in your AppServiceProvider. Which will make the class available via app(GlobalSearch::class) wherever you want in the application. This means the query will only run once during the initialization process and maintain the data.
More info about singleton bindings: https://laravel.com/docs/8.x/container#binding-a-singleton
$this->app->singleton(GlobalSearch::class, function ($app) {
return new GlobalSearch(auth()->user()->team);
});

Related

Adding filter to eloquent resource to attach relationship conditionally

Laravel 5.8
PHP 7.4
I want to load the relationships conditionally like
http://127.0.0.1:8000/api/posts
and
http://127.0.0.1:8000/api/posts/1 are my end points now, I want to load comments like
http://127.0.0.1:8000/api/posts/?include=comments and
http://127.0.0.1:8000/api/posts/1/?include=comments
If the query parameter is there, only then it should load comments with posts or it should load only posts/post
I am doing this by referring a blog post
now, RequestQueryFilter
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
class RequestQueryFilter
{
public function attach($resource, Request $request = null)
{
$request = $request ?? request();
return tap($resource, function($resource) use($request) {
$this->getRequestIncludes($request)->each(function($include) use($resource) {
$resource->load($include);
});
});
}
protected function getRequestIncludes(Request $request)
{
// return collect(data_get($request->input(), 'include', [])); //single relationship
return collect(array_map('trim', explode(',', data_get($request->input(), 'include', [])))); //multiple relationships
}
}
and in helper
<?php
if ( ! function_exists('filter') ) {
function filter($attach)
{
return app('filter')->attach($attach);
}
}
?>
in PostController
public funciton show(Request $request, Post $post) {
return new PostResource(filter($post));
}
but when I am trying to retrieve
http://127.0.0.1:8000/api/posts/1/?include=comments getting no comments, with no error in log
A work around will be PostResource
public function toArray($request)
{
// return parent::toArray($request);
$data = [
'id' => $this->id,
'name' => $this->title,
'body' => $this->content,
];
$filter = $request->query->get('include', '');
if($filter){
$data[$filter] = $this->resource->$filter;
}
return $data;
}
I want to load the relationships conditionally like
Lazy Eager Loading using the load() call
The Lazy Eager Loading accomplishes the same end results as with() in Laravel, however, not automatically. For example:
?include=comments
// Get all posts.
$posts = Post::without('comments')->all();
if (request('include') == 'comments')) {
$posts->load('comments');
}
return PostResource::collection($posts);
Alternativelly, you could require the include query string to be an array:
?include[]=comments&include[]=tags
// Validate the names against a set of allowed names beforehand, so there's no error.
$posts = Post::without(request('includes'))->all();
foreach (request('includes') as $include) {
$posts->load($include);
}
return PostResource::collection($posts);
The call without() is only required in case you defined your model to automatically eager load the relationships you want to conditionally load.
With all data filtered in Controller, just make sure to display only loaded relations in your PostResource
public function toArray($request) {
$data = [...];
foreach ($this->relations as $name => $relation)
{
$data[$name] = $relation;
}
return $data;
}
I would create a custom resource for the posts with
php artisan make_resource
command.
E.g. PostResource.
The toArray function of the resource must return the data.
PostResource.php
public function toArray($request){
$data =['title' => $this->resource->title,
'body' => $this->resource->body,
'images' => new ImageCollection($this->whenLoaded('images')),
];
$filter = $request->query->get('filter', '');
if($filter){
$data['comments'] => new CommentCollection($this->resource->comments);
}
return $data;
}
Also, for collections, you need to create a ResourceCollection.
PostResourceCollection.php
class PostResourceCollection extends ResourceCollection
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request
* #return array
*/
public function toArray($request)
{
return [
'data' => $this->collection,
];
}
}
In your controller:
PostsController.php
//show one post
public function show(Post $post, Request $request)
{
/**this response is for API or vue.js if you need to generate view, pass the resource to the view */
return $this->response->json( new PostResource($post));
}
//list of posts
public function index(Request $request)
{
$posts = Post::all();
/**this response is for API or vue.js if you need to generate view, pass the resource to the view */
return $this->response->json( new PostResourceCollection($posts));
}
Partial Solution
It will need a small change in resource class
public function toArray($request)
{
// return parent::toArray($request);
$data = [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'comments' => new CommentCollection($this->whenLoaded('comments')),
'images' => new ImageCollection($this->whenLoaded('images')),
];
return $data;
}
and it will load comments and images if loaded and that depends on the include query parameter, if that is not included, it will not load the relationship.
However,
In post collection
return [
'data' => $this->collection->transform(function($post){
return [
'id' => $post->id,
'title' => $post->title,
'body' => $post->body,
'comments' => new CommentCollection($post->whenLoaded('comments')),
'images' => new ImageCollection($post->whenLoaded('images')),
];
}),
];
will results in
"Call to undefined method App\Models\Customer::whenLoaded()",, if anyone suggests a complete solution, it will be a great help, if I will able to do, it I will update here.

Relational search using laravel-scout-tntsearch-driver package for Laravel Scout

I'm using the laravel-scout-tntsearch-driver package for Laravel Scout. I have implemented it, and everything works fine. But now I want to do a relational search. I have cities that have many companies.
City.php
public function companies()
{
return $this->hasMany(Company::class);
}
Company.php
public function city()
{
return $this->belongsTo(City::class);
}
public function toSearchableArray()
{
return [
'id' => $this->id,
'title' => $this->title
];
}
Now the search is working only for all companies.
Company::search('bugs bunny')->get();
Also where clauses don't work here. I want something like this:
Route::get('/search/{city}', function (\App\City $city) {
$companies = $city->companies()->search('bugs bunny');
});
I think you got the idea. Thanks!
First, I would move the logic to a controller. After doing that, you could have a method in the controller that implements the required search like this:
public function search(City $city){
$companiesInCity = Company::where('city_id', $city->id)->get('id')->toArray();
$companiesMatching = Company::search('bugs bunny')->whereIn('id', $companiesInCity)->get();
return view('search.result', [
'result' => $result
]);
}
And finally call the function from your routeing web.php.
Working in my dev env.
Hope this helps!

Laravel 5.2 - Using SetIdAttribute() Mutator To Set Other Value

I am currently creating a blog where each Post row in my database will have a unique hash attribute that is based of the post's id (incrementing, always unique).
This my Post model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Hashids;
class Post extends Model
{
public function setTitleAttribute($value)
{
$this->attributes['title'] = $value;
if (! $this->exists) {
$this->attributes['slug'] = str_slug($value);
}
}
public function setIdAttribute($value) {
$this->attributes['id'] = $value;
$this->attributes['hash'] = Hashids::encode($value);
}
}
When I run this factory
$factory->define(App\Post::class, function (Faker\Generator $faker) {
return [
'title' => $faker->sentence(mt_rand(3, 10)),
'content' => join("\n\n", $faker->paragraphs(mt_rand(3, 6))),
'author' => $faker->name,
'category' => rand(1, 20),
];
});
The setIdAttribute($value) function is getting called, but my hash attribute is not being set. I am not sure if it is getting overwritten or what.
If I move the line
$this->attributes['hash'] = Hashids::encode($value);
to the function
public function setTitleAttribute($value)
and encode the title attribute it works fine, but I want to encode the 'id' attribute. Any idea how I would do this?
You can add the following to your model:
/**
* Events
*/
public static function boot()
{
parent::boot();
static::created(function($model)
{
$model->hash = Hashids::encode($model->id);
$model->slug = str_slug($model->title);
}
}
It's likely setIdAttribute($value) isn't being called until after the insert runs because it doesn't know the ID until then.
The real issue is you can't set a hash of the id in the same query because the id isn't going to be known (assuming it's auto_incrementing) until after the insert.
Because of this, the best you can probably do here is fire some code on the model's saved event.
In that model, you can probably do something like...
public static function boot()
{
parent::boot();
static::flushEventListeners(); // Without this I think we have an infinite loop
static::saved(function($post) {
$post->hash = Hashids:encode($post->id);
$post->save();
});
}

Testing laravel repository which has model as a dependency

Problem is that i can't test one function, because it is touching other functions of the same repository.
Do I need to test one function in isolation from other functions in same repository, or it is normal that one function can access other functions in same repository ?
If function needs to be tested in isolation from other, how it can be done, because I don't understand how I can mock repository in which I'm working. I understand how to mock dependencies, but how to mock other functions in same repository ?
Am I mocking model correctly in setUp method in the test?
Code:
Real world binding of and repository:
// Bind User repository interface
$app->bind('MyApp\Repositories\User\UserInterface', function () {
return new EloquentUser(new User);
});
EloquentUser.php:
public function __construct(Model $user)
{
$this->user = $user;
}
public function findById($id)
{
return $this->user->find($id);
}
public function replace($data)
{
$user = $this->findById($data['user']['id']);
// If user not exists, create new one with defined values.
if ( ! $user) {
return $this->create($data);
} else {
return $this->update($data);
}
}
public function create($data)
{
$user = $this->user->create($data['user']);
if ($user) {
return $this->createProfile($user, $data['profile']);
} else {
return false;
}
}
private function createProfile($user, $profile)
{
return $user->profile()->create($profile);
}
public function update($user, $data)
{
foreach ($data['user'] as $key => $value) {
$user->{$key} = $value;
}
if (isset($data['profile']) && count($data['profile']) > 0) {
foreach ($data['profile'] as $key => $value) {
$user->profile->$key = $value;
}
}
return ($user->push()) ? $user : false;
}
EloquentUserTest.php
public function setUp()
{
parent::setUp();
$this->user = Mockery::mock('Illuminate\Database\Eloquent\Model', 'MyApp\Models\User\User');
App::instance('MyApp\Models\User\User', $this->user);
$this->repository = new EloquentUser($this->user);
}
public function testReplaceCallsCreateMethod()
{
$data = [
'user' => [
'id' => 1,
'email' => 'test#test.com',
],
'profile' => [
'name' => 'John Doe',
'image' => 'abcdef.png',
],
];
// Mock the "find" call that is made in findById()
$this->user->shouldReceive('find')->once()->andReturn(false);
// Mock the "create" call that is made in create() method
$this->user->shouldReceive('create')->once()->andReturn(true);
// Run replace method that i want to test
$result = $this->repository->replace($data);
$this->assertInstanceOf('Illuminate\Database\Eloquent\Model', $result, 'Should be an instance of Illuminate\Database\Eloquent\Model');
}
When running this test I got:
Fatal error: Call to a member function profile() on a non-object in C:\Htdocs\at.univemba.com\uv2\app\logic\Univemba\Repositories\User\EloquentUser.php on line 107
So it means that Test is trying to touch function in EloquentUser.php:
private function createProfile($user, $profile)
{
return $user->profile()->create($profile);
}
Do I need to mock createProfile ? because profile() cant be found. And if I need to do this, how can i do it because this function is in same repository that i'm testing?
Question is solved.
Just needed to create one more Model instance and pass it in mocked method.
My Working setUp method:
public function setUp()
{
parent::setUp();
$this->user = Mockery::mock('MyApp\Models\User\User');
App::instance('MyApp\Models\User\User', $this->user);
$this->repository = new EloquentUser($this->user);
}
Working test method:
public function testReplaceCallsCreateMethod()
{
$data = [
'user' => [
'id' => 1,
'email' => 'test#test.com',
'password' => 'plain',
],
'profile' => [
'name' => 'John Doe',
'image' => 'abcdef.png',
],
];
// Mock Model's find method
$this->user->shouldReceive('find')->once()->andReturn(false);
// Create new Model instance
$mockedUser = Mockery::mock('MyApp\Models\User\User');
// Mock Models profile->create and pass Model as a result of a function
$mockedUser->shouldReceive('profile->create')->with($data['profile'])->andReturn($mockedUser);
// Pass second instance Model as a result
$this->user->shouldReceive('create')->once()->andReturn($mockedUser);
// Now all $user->profile is properly mocked and will return correct data
$result = $this->repository->replace($data);
$this->assertInstanceOf('Illuminate\Database\Eloquent\Model', $result, 'Should be an instance of Illuminate\Database\Eloquent\Model');
}

How to handle multidimensional output with (nested) lists using the Zend\Db\TableGateway with a mapper in Zend Framework 2?

I'm developing a RESTful ZF2 based application and using a TableGateway implementation (subclass of the Zend\Db\TableGateway) in combination with a simple mapper for the model, similar to the Album example of the ZF2 manual.
Table class
<?php
namespace Courses\Model;
use ...
class CourseTable {
protected $tableGateway;
public function __construct(TableGateway $tableGateway) {
$this->tableGateway = $tableGateway;
}
public function findOnceByID($id) {
$select = new Select();
$where = new Where();
$select->columns(array(
'id',
'title',
'details',
));
$select->from($this->tableGateway->getTable());
$select->where($where, Predicate::OP_AND);
$resultSet = $this->tableGateway->selectWith($select);
return $resultSet;
}
}
Mapper class
<?php
namespace Courses\Model;
use ...
class CourseDetails implements ArraySerializableInterface {
public $id;
public $title;
public $details;
public function exchangeArray(array $data) {
$this->id = (isset($data['id'])) ? $data['id'] : null;
$this->title = (isset($data['title'])) ? $data['title'] : null;
$this->details = (isset($data['details'])) ? $data['details'] : null;
}
public function getArrayCopy() {
return get_object_vars($this);
}
}
Controller
<?php
namespace Courses\Controller;
use ...
class CoursesController extends RestfulController // extends AbstractRestfulController
{
protected $acceptCriteria = array(
'Zend\View\Model\JsonModel' => array(
'application/json',
),
'Zend\View\Model\FeedModel' => array(
'application/rss+xml',
),
);
private $courseTable;
public function get($id)
{
$course = $this->getCourseTable()->findOnceByID($id)->current();
$viewModel = $this->acceptableViewModelSelector($this->acceptCriteria);
$viewModel->setVariables(array('data' => array(
'id' => $courseDetails->id,
'title' => $courseDetails->title,
'details' => $courseDetails->details
)));
return $viewModel;
}
...
}
It's working for a shallow output like this:
{
"data":{
"id":"123",
"title":"test title",
"details":"test details"
}
}
But now I need a multidimensional output with nested lists like this:
{
"data":{
"id":"123",
"title":"test title",
"details":"test details",
"events":{
"count":"3",
"events_list":[ <- main list
{
"id":"987",
"date":"2013-07-20",
"place":"Berlin",
"trainers":{
"count":"1",
"trainers_teamid":"14",
"trainers_teamname":"Trainers Team Foo",
"trainers_list":[ <- nested list
{
"id":"135",
"name":"Tom"
}
]
}
},
{
"id":"876",
"date":"2013-07-21",
"place":"New York",
"trainers":{
"count":"3",
"trainers_teamid":"25",
"trainers_teamname":"Trainers Team Bar",
"trainers_list":[ <- nested list
{
"id":"357",
"name":"Susan"
},
{
"id":"468",
"name":"Brian"
},
{
"id":"579",
"name":"Barbara"
}
]
}
},
{
"id":"756",
"date":"2013-07-29",
"place":"Madrid",
"trainers":{
"count":"1",
"trainers_teamid":"36",
"trainers_teamname":"Trainers Team Baz",
"trainers_list":[ <- nested list
{
"id":"135",
"name":"Sandra"
}
]
]
}
]
}
}
}
How / where should I assemble the data to this structure? Directly in the mapper, so that it contains the whole data? Or should I handle this with multiple database requests anb assemple the structure in the controller?
What you are trying to accomplish has nothing to do with the TableGateway-Pattern. The TableGateway-Pattern is there to gain access to the Data of one specified Table. This is one of the reasons why in ZF2 you no longer have the option to findDependantRowsets(). It's simply not the TableGateways Job to do so.
To achieve what you are looking for you have pretty much two options:
1. Joined Query
You could write a big query that joins all respective tables and then you'd manually map the output into your desired JSON Format.
2. Multiple Queries
A little less performant approach (looking at the SQL side of things) but "easier" to "map" into your JSON Format.
To give some insight, Doctrine would go with the multiple query approach by default. This is mostly (i guess!) done to provide features that would work on every data backend possible rather than just a couple of SQL Versions...
Service Class
Since you're wondering about the assembling of the json / array, i would set it up like this
'service_manager' => array(
'factories' => array(
'MyEntityService' => 'Mynamespace\Service\Factory\MyEntityServiceFactory'
)
)
// MyEntityServiceFactory.php
// assuming you only need one dependency! more lines for more dependencies ;)
class MyEntityServiceFactory implements FactoryInterface {
public function createService(ServiceLocatorInterface $serviceLocator) {
return new MyEntityService($serviceLocator->get('YourTableGateway'));
}
}
// Your SERVICE Class
class MyEntityService {
// do constructor and stuff to handle dependency
public function someBigQueryAsArray() {
// Query your Gateway here and create the ARRAY that you want to return,
// basically this array should match your json output, but have it as array
// to be used for other stuff, too
}
}
// lastly your controller
public function someAction() {
$service = $this->getServiceLocator()->get('MyEntityService');
$data = $service->someBigQueryAsArray();
// do that viewmodel selector stuff
// ASSUMING $data is a array of more than one baseObject
// i did this in my app to produce the desired valid json output, there may be better ways...
if ($viewModel instanceof JsonModel) {
foreach($data as $key => $value) {
$viewModel->setVariable($key, \Zend\Json\Json::encode($value));
}
return $viewModel;
}
// Handle other ViewModels ....
}

Categories