How to Paginate loaded relation with Laravel API resources - laravel

I need to load model relations in it's resource and paginate them.
In my case i have Category and Path models, plus CategoryResource and PathResource
The toArray method of CategoryResource is like below:
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'order' => $this->order,
'paths' => PathResource::collection($this->whenLoaded('paths'))
];
}
and toArray method of PathResource is like below:
public function toArray($request)
{
return parent::toArray($request);
}
Question is how can i load and paginate related Path's in my CategoryResource?

I had same problem and solved it this way:
Prerequisites
You must have/create a resource for Path model i.e. PathResource
to create one use this command:
php artisan make:resource PathResource
Solution
The solution is to use laravel paginate on relation and use transform method on the paginated collection to convert it's items to your resource.
First Step
Create a base class for paginating any resource in your app, using this command:
php artisan make:resource PaginatedCollection -c
Edit the PaginatedCollection and add following codes:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class PaginatedCollection extends ResourceCollection
{
/**
* An array to store pagination data that comes from paginate() method.
* #var array
*/
protected $pagination;
/**
* PaginatedCollection constructor.
*
* #param mixed $resource paginated resource using paginate method on models or relations.
*/
public function __construct($resource)
{
$this->pagination = [
'total' => $resource->total(), // all models count
'count' => $resource->count(), // paginated result count
'per_page' => $resource->perPage(),
'current_page' => $resource->currentPage(),
'total_pages' => $resource->lastPage()
];
$resource = $resource->getCollection();
parent::__construct($resource);
}
/**
* Transform the resource collection into an array.
* now we have data and pagination info.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
// our resources
'data' => $this->collection,
// pagination data
'pagination' => $this->pagination
];
}
}
Second Step
make a collection resource for your model and extend PaginatedCollection
instead of default ResourceCollection.
Run this command to do so:
php artisan make:resource PathCollection -c
Now edit your new collection class PathCollection and override toArray method:
/**
* Transform the resource collection into an array.
*
* In this method use your already created resources
* to avoid code duplications
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
// Here we transform any item in paginated items to a resource
'data' => $this->collection->transform(function ($path) {
return new PathResource($path);
}),
'pagination' => $this->pagination,
];
}
Final Step
In your CategoryResource use PathCollection like this:
return [
'id' => $this->id,
'name' => $this->name,
'slug' => $this->slug,
'order' => $this->order,
'paths' => new PathCollection(
new LengthAwarePaginator(
$this->whenLoaded('paths'),
$this->paths_count,
10
)
),
];
and make sure you import LengthAwarePaginator class:
use Illuminate\Pagination\LengthAwarePaginator;
Usage
$category = Category::with('paths')->withCount('paths')->find(1);
return new CategoryResource($category);

You should probably checkout the documentation on Resources and ResourceCollections. ResourceCollections will allow you to easily paginate your resources. Api Resource Collection Pagination Documentation

Related

Eloquent Select Query On JSON Columns

I have a JSON column for properties table. Below is the eloquent query to pick specific fields.
Company::select('id', 'information')
->with([
'properties' => function($sql) {
return $sql->select('id', 'company_id', 'information->>status');
}
])
->get();
Is there a way to replace the response with the actual key?
The simplest method to get the key would be to to give it an alias. So something like
$sql->select('id', 'company_id', 'information->status as status');
$sql->select('id', 'company_id', 'information->status as information_status');
$sql->select('id', 'company_id', 'information->status as information.status');
Edit
If you wish to maintain the structure of the response, then its better to create a Resource and let it handle the structure. So
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
class Properties extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
$information = ['status'=>optional($this->information)['status']];
return [
'id' => $this->id,
'company_id' => $this->company_id,
'information' => $information,
];
}
}
Similarly create a Resource for Company with the fields you want.

how to use apiReources method with `only`?

I'm creating an api with Laravel and I am looking for an easy lazy way to to register Api resources.
I'm currently defining my routes like this:
Route::apiResource('categories', 'CategoryController')->only(['index', 'show']);
I checked Laravel's controller documentation and I saw apiResources method which I can create multiple api resources at once.
the goal:
is to be able to use apiResources with only method like this
Route::apiResources(['categories' => 'CategoryController', 'products' => 'ProductController'])->only(['index', 'show']);
current result:
Call to a member function only() on null
long story short (if you don't want to read the whole story) you can just do it like this:
Route::apiResources(['brands' => 'BrandController', 'categories' => 'CategoryController'], ['only' => ['index', 'show']]);
When I was writing the question it passed to my mind to check the apiResources declaration and I found this:
/**
* Register an array of API resource controllers.
*
* #param array $resources
* #param array $options
* #return void
*/
public function apiResources(array $resources, array $options = [])
{
foreach ($resources as $name => $controller) {
$this->apiResource($name, $controller, $options);
}
}
and since it is using apiResource under the hood and it is passing options parameter I can check what are these options
/**
* Route an API resource to a controller.
*
* #param string $name
* #param string $controller
* #param array $options
* #return \Illuminate\Routing\PendingResourceRegistration
*/
public function apiResource($name, $controller, array $options = [])
{
$only = ['index', 'show', 'store', 'update', 'destroy'];
if (isset($options['except'])) {
$only = array_diff($only, (array) $options['except']);
}
return $this->resource($name, $controller, array_merge([
'only' => $only,
], $options));
}

The effects of adding a Service Layer to a Laravel application

I have been developing with Zend Framework for a number of years and am now learning Laravel.
In my previous applications I usually have a Service Layer that is called by controllers. The Service Layer sits across the top of a Mapper and a Domain Model and is responsible for some application logic, raising events, some input filtering, etc.
Is there any reason why I should not implement a Service Layer in Laravel? In the examples that I have seen so far, controllers work directly with domain objects (or more accurately, active records).
If my Laravel controllers called my Service Layer, would I lose any of the advantages of Laravel? (As far as I can see I can still use Route/Model binding).
As a secondary question - what would be the best way to implement my Service Layer? As a collection of Service Providers, perhaps?
I also switched to Laravel coming from Zend and missed my Services. To sooth myself I have implemented a Service namespace which sits in namespace App\Services. In there I do all my Model or data handeling I don't want to see in my controller etc.
An example of my controller layout:
<?php
namespace App\Http\Controllers;
use App\Services\Contact as ContactService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Lang;
class IndexController extends Controller
{
/**
* Create a new controller instance.
*
* #param Request $request
* #return void
*/
public function __construct(Request $request)
{
$this->_request = $request;
}
/**
* Standard contact page
*
* #return contact page
*/
public function contact(ContactService $contactService)
{
$errors = null;
$success = false;
if ($this->_request->isMethod('post')) {
$validator = $contactService->validator($this->_request->all());
if ($validator->fails()) {
$errors = $validator->errors();
} else {
$contactService->create($validator->getData());
$success = true;
}
}
return view('pages/contact', ['errors' => $errors, 'success' => $success]);
}
}
The services return validators, handle cruds, basically do everything that I don't want to see in my Controller just like I had it in my Zend projects.
Example of Service:
<?php
namespace App\Services;
use Validator;
use Mail;
use App\Models\Contact as ContactModel;
class Contact
{
/**
* Get a validator for a contact.
*
* #param array $data
* #return \Illuminate\Contracts\Validation\Validator
*/
public function validator(array $data)
{
return Validator::make($data, [
'email' => 'required|email|max:255',
'phone' => 'max:255',
'firstName' => 'required|max:255',
'lastName' => 'required|max:255',
'message' => 'required'
]);
}
/**
* Create a new contact instance after a valid form.
*
* #param array $data
* #return ContactModel
*/
public function create(array $data)
{
//Handle or map any data differently if needed, just for illustration
$data = [
'email' => $data['email'],
'firstName' => $data['firstName'],
'lastName' => $data['lastName'],
'language' => $data['language'],
'phone' => $data['phone'],
'message' => $data['message']
];
// Send an email
Mail::send('emails.contact', ['data' => $data], function ($m) use ($data) {
$m->from(config('mail.from.address'), config('mail.from.name'));
$m->to(env('MAIL_TO', 'hello#world.com'), env('MAIL_TO'))->subject('Contact form entry from: ' . $data['firstName']);
});
return ContactModel::create($data);
}
}
See this post: “Design Pattern : Service Layer with Laravel 5” #Francoiss https://m.dotdev.co/design-pattern-service-layer-with-laravel-5-740ff0a7b65f.. in addition to the above answer you can also encapsulate the validation code inside a separate validator class

How can you set boosts and filters with Laravel Scout and ElasticSearch?

I am building a search integration with Laravel Scout and Elasticsearch. I am trying to figure out how I can refine my queries for boosting.
Is it possible to do this with Laravel Scout or will I need to revert back to using the ElasticSearch PHP library directly?
Actually it can be simply done with custom Scout Engine.
Let's call it ElasticqueryEngine, for example, and extend it from default ElasticsearchEngine:
<?php
namespace App\Libs\Scout\Engines;
use Laravel\Scout\Builder;
use Laravel\Scout\Engines\ElasticsearchEngine;
class ElasticqueryEngine extends ElasticsearchEngine
{
/**
* Perform the given search on the engine.
*
* #param Builder $query
* #param array $options
* #return mixed
*/
protected function performSearch(Builder $query, array $options = [])
{
if (!is_array($query->query)) {
return parent::performSearch($query, $options);
}
$searchQuery = [
'index' => $this->index,
'type' => $query->model->searchableAs(),
'body' => [
'query' => $query->query,
],
];
if (array_key_exists('size', $options)) {
$searchQuery = array_merge($searchQuery, [
'size' => $options['size'],
]);
}
if (array_key_exists('from', $options)) {
$searchQuery = array_merge($searchQuery, [
'from' => $options['from'],
]);
}
return $this->elasticsearch->search($searchQuery);
}
}
Add new service provider to register new ElasticqueryEngine (or do it in any of the existing service providers):
<?php
namespace App\Providers;
use Laravel\Scout\EngineManager;
use Illuminate\Support\ServiceProvider;
use Elasticsearch\ClientBuilder as Elasticsearch;
use App\Libs\Scout\Engines\ElasticqueryEngine;
class ElasticqueryServiceProvider extends ServiceProvider
{
/**
* Perform post-registration booting of services.
*
* #return void
*/
public function boot()
{
resolve(EngineManager::class)->extend('elasticquery', function () {
return new ElasticqueryEngine(
Elasticsearch::fromConfig(config('scout.elasticsearch.config')),
config('scout.elasticsearch.index')
);
});
}
/**
* Register bindings in the container.
*
* #return void
*/
public function register()
{
//
}
}
Don't forget to add new service provider in config/app.php:
'providers' => [
// ...
Laravel\Scout\ScoutServiceProvider::class,
App\Providers\ElasticqueryServiceProvider::class,
],
And change driver to "elasticquery" in config/scout.php or .env (SCOUT_DRIVER=elasticquery)
After all, you can search by any queries from https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html:
$query = [
'simple_query_string' => [
'query' => 'findme',
'fields' => [
'title^5',
'description',
],
],
];
$posts = Posts::search($query)->get();
// also you can use default ElasticsearchEngine query
$posts = Posts::search('findme')->where('user_id', 1)->get();
Laravel Scout does not support advanced Elasticsearch features. I ended up sticking with Scout for all of the index creation and updating based on model events, but I switched my search over to utilize the ElasticSearch library directly.

Display list and details using separate controllers in Laravel

I'm using the following two routes in my app, but they are failing. (only one works when I switch the positions). Notice that I'm using two separate controllers. What's the correct way of specifying routes for a scenario like this?
Route::controller('schedulers', 'SchedulersController', [
'getIndex' => 'schedulers.index',
'getUpdate' => 'schedulers.edit'
]);
Route::controller('schedulers/{schedulerId}', 'ReportsController', [
'getIndex' => 'schedulers.reports',
]);
You can add route prefix
Example :
Route::group(['prefix' => 'schedulers'], function() {
Route::controller('/{schedulerId}', 'ReportersController', [
'getIndex' => 'schedulers.reports',
]);
Route::controller('/', 'SchedulersController', [
'getIndex' => 'schedulers.index',
'getUpdate' => 'schedulers.edit'
]);
});
Try to user resource (routes.php):
Route::resource('schedulers', SchedulersController::class);
After this create SchedulersController with two methods: show, index:
<?php
namespace App\Http\Controllers;
class SchedulersController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Display the specified resource.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
}
schedulers => SchedulersController::index
schedulers/1 => SchedulersController::show

Resources