I've started creating a RESTful API (well, I did my best, I'm trying to follow the patterns) and I have stumbled upon a scenario that I'm not really sure how to handle. I will explain the current structure:
My application has 4 controllers:
Customers
Payments
Log
Taking as example the Customers controller, I have defined the following actions:
GET /customers: returns a list of customers
POST /customers: creates a new customer
GET /customers/{id}: returns the customer with the provided id
PUT /customers/{id}: updates the customer with the provided id
DELETE /customers/{id}: destroys the customer
This is the full code of the Customer controller:
namespace App\Http\Controllers;
use App\Customer;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
return Customer::all();
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$customer = Customer::create($request->all());
return response()->json($customer, 201);
}
/**
* Display the specified resource.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function show(Customer $customer)
{
return $customer;
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function update(Request $request, Customer $customer)
{
$customer->update($request->all());
return response()->json($customer, 200);
}
/**
* Remove the specified resource from storage.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function destroy(Customer $customer)
{
$customer->delete();
return response()->json(null, 204);
}
}
The code is very similar in the other controllers. It's also important to note that:
A Customer can have multiple Payments
A Customer can have multiple records in the Log
The problem starts here:
I need to display in the front-end a summary page with all customer data (name, email, registration date, etc) and a box showing the number of payments made and another box showing the number of entries in the Log.
Do I need to make 3 requests? (One to /customers/id, other to customers/id/payments and other to customers/id/logs)
If I return all the customer related data in the customers/id call, am I breaking the RESTful convention?
I am using apigility, but my answer still will be related to your question. According to the REST terminology (which could be find here https://apigility.org/documentation/intro/first-rest-service#terminology ) You are talking about entity and collection.
/customers/id - entity,
/customers/id/payments - collection,
/customers/id/logs - collection.
These are 3 different requests. So, yes, you need make 3 different requests.
But, to be honest, if you don't need pagination over payments and logs you can have only one request to /customers/id and within response you can have fields with array
{
"_links": {
"self": {
"href": "http://localhost:8080/status/3c10c391-f56c-4d04-a889-bd1bd8f746f0"
}
},
"id": "3c10c391-f56c-4d04-a889-bd1bd8f746f0",
...
_payments: [
...
],
_logs: [
...
],
}
Upd (duplicate from comment for future visitors).
Also, you should pay attention to DTO. I suppose this link will be interesting https://stackoverflow.com/a/36175349/1581741 .
Upd2.
At current moment I treat your collection /customers/id/payments like this:
/payments?user_id=123
where user_id is filtering field on payments table.
I think your problem that you confuse your REST API with your database. They don't have to follow the same structure. You can easily return the whole nested JSON for GET /customers/{id} if that's what you need from your REST API.
Related
Based on a private messaging app, my user entity currently have two methods to retrieve messages :
One to get the messages sent by the user
/**
* #ORM\OneToMany(targetEntity="App\Entity\MessagePrive", mappedBy="emetteur")
*/
private $messagesPrivesEmis;
/**
* #return Collection|MessagePrive[]
*/
public function getMessagesPrivesEmis(): Collection {
return $this->messagesPrivesEmis;
}
and another one to get the messages received from other users
/**
* #ORM\OneToMany(targetEntity="App\Entity\MessagePrive", mappedBy="recepteur")
*/
private $messagesPrivesRecus;
/**
* #return Collection|MessagePrive[]
*/
public function getMessagesPrivesRecus(): Collection {
return $this->messagesPrivesRecus;
}
The first method get the messages where emetteur is equal to user id, while the second get the messages where recepteur is equal to user id. Both are Symfony default methods
Is it possible to "merge" those two methods so it get all messages sent and received by the user in one single query?
Or should I resort to custom DQL?
public function getMerged(): Collection {
return new ArrayCollection(
array_merge(this->messagesPrivesEmis->toArray(), $this->messagesPrivesRecus->toArray())
);
}
I have a method that needs to pull in information from three related models. I have a solution that works but I'm afraid that I'm still running into the N+1 query problem (also looking for solutions on how I can check if I'm eager loading correctly).
The three models are Challenge, Entrant, User.
Challenge Model contains:
/**
* Retrieves the Entrants object associated to the Challenge
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
Entrant Model contains:
/**
* Retrieves the Challenge object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function challenge()
{
return $this->belongsTo('App\Challenge', 'challenge_id');
}
/**
* Retrieves the User object associated to the Entrant
* #return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('App\User', 'user_id');
}
and User model contains:
/**
* Retrieves the Entrants object associated to the User
* #return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function entrants()
{
return $this->hasMany('App\Entrant');
}
The method I am trying to use eager loading looks like this:
/**
* Returns an array of currently running challenges
* with associated entrants and associated users
* #return array
*/
public function liveChallenges()
{
$currentDate = Carbon::now();
$challenges = Challenge::where('end_date', '>', $currentDate)
->with('entrants.user')
->where('start_date', '<', $currentDate)
->where('active', '1')
->get();
$challengesObject = [];
foreach ($challenges as $challenge) {
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all();
$entrantsObject = [];
foreach ($entrants as $entrant) {
$user = $entrant->user;
$entrantsObject[] = [
'entrant' => $entrant,
'user' => $user
];
}
$challengesObject[] = [
'challenge' => $challenge,
'entrants' => $entrantsObject
];
}
return $challengesObject;
}
I feel like I followed what the documentation recommended: https://laravel.com/docs/5.5/eloquent-relationships#eager-loading
but not to sure how to check to make sure I'm not making N+1 queries opposed to just 2. Any tips or suggestions to the code are welcome, along with methods to check that eager loading is working correctly.
Use Laravel Debugbar to check queries your Laravel application is creating for each request.
Your Eloquent query should generate just 3 raw SQL queries and you need to make sure this line doesn't generate N additional queries:
$entrants = $challenge->entrants->load('user')->sortByDesc('current_total_amount')->all()
when you do ->with('entrants.user') it loads both the entrants and the user once you get to ->get(). When you do ->load('user') it runs another query to get the user. but you don't need to do this since you already pulled it when you ran ->with('entrants.user').
If you use ->loadMissing('user') instead of ->load('user') it should prevent the redundant call.
But, if you leverage Collection methods you can get away with just running the 1 query at the beginning where you declared $challenges:
foreach ($challenges as $challenge) {
// at this point, $challenge->entrants is a Collection because you already eager-loaded it
$entrants = $challenge->entrants->sortByDesc('current_total_amount');
// etc...
You don't need to use ->load('user') because $challenge->entrants is already populated with entrants and the related users. so you can just leverage the Collection method ->sortByDesc() to sort the list in php.
also, You don't need to run ->all() because that would convert it into an array of models (you can keep it as a collection of models and still foreach it).
I'm banging my head to find a solution to this but still i'm unable, I'm looking for a design vice solution not a hack to fix the issue.
I have following classes
class CourseService{
public function getCourse($courceId){
$course = $this->courseRepo->getCourse($courseId);
$restrictions = $this->invoiceService->getRestrictions($course->courseid);
$course->restrictions = [];
if($restrictions != null){
$course->restrictions = $restrictions;
}
}
}
Now this course service is injected in the constructor of the StudentService because when students need to enroll to a cource i use this course service there.
also you can see that I have used CourseRepo to get Course object and then InvoiceService to say which fields are restricted to update, basically restrictions attributes gives an array of strings defining which fields are not allowed to edit and I expect UI developer will use it to disable those fields, and I had to inject InvoiceService because there are some processing to do to the raw db records that are fetched from the InvoiceRepo so invoice repo is encapsulated in the invoiceService
now lets look at the InvoiceService
Class InvoiceService{
public function getAmountToPay($courseid, $studentid){
//now I need to inject StduentService inorder to get student info which needed for the calculation
}
}
but I can't inject StudentService into here because StudentService -> CourceService -> InvoiceService
Options I see and the consequences
One option I see is to get rid of InvoiceService from the CourseService and use InvoiceService in the place where the getCourse() get called and then modify the result but the problem is, CourseService is used mainly in controllers and next thing is that getCourse() get called from many controllers and service and expects the restrictions to be there so if I want to get rid of the InvoiceService then I'll have many places to add the removing lines and it crates a code repetition.
I can move getAmountToPay() to student service but then that service has already doing many student related tasks and i'm happy to extract just the invoice part to another service so I have a clear place to look when I need to check for bugs on invoices.
Student service:
First of all you have to see - actually to decide - that a student service uses an invoice service, not the reciprocal. When I enroll myself as a history student, I go to the registration/students office first. They are calling the financial/invoice office to ask about how much should I pay. The financial office checks in the database and returns the response regarding the amount to be payed by me.
Course service:
...The time passed by. Now I'm a student. I don't need to go to the registration office anymore. If I have to know something about my courses I go to the secretariat/course service. They'll give me all the informations I need about my courses. But, if I want to visit some special archeology course, where one must pay something, the course service will call the financial/invoice service to ask about that for me. They, in turn, will return the infos. The same applies if the course service wants to know about some financial restrictions I should have: they call the financial/invoice service.
Invoice service - student service, invoice service - course service:
Now, what should happen, if the invoice service needs infos about a student or a course? Should it call the student service, or the course service for that? The answer is no. The invoice service should receive a student id, a course id, a domain object Student, or a domain object Course as constructor/methods dependencies, but not the corresponding service(s). And it will fetch the infos it needs by itself. More of it, the invoice service should work with its specific invoice/financial tables, not with the course tables or the student details tables (except their id's).
Conclusions:
To enroll a student is the job of the StudentService. Though the
CourseService can assist the enrollment process.
StudentService verifies the amount to be paid by a student by calling
the InvoiceService. I know you don't want to have getAmountToPay()
inside the StudentService, but it's a natural workflow. You may think
of separate the other many things, for which the StudentService is
responsible, to another services.
The CourseService is responsible for finding a course, together with
the course restrictions, for which it calls the InvoiceService. So,
the CourseService will be assisted by the InvoiceService.
Down under I passed you the PHP version of my vision. I renamed some functions, to give you a better perspective.
Good luck!
P.S: I hope I understood right, that the sense of "invoice sevice" is a "financial department" one. Sorry, but I'm not a native english speaker, so I can't know all the senses.
<?php
class StudentService {
protected $courseService;
protected $invoiceService;
/**
* Even if the course service uses the invoice service,
* doesn't mean that the student service shouldn't use it too.
*
* #param CourseService $courseService
* #param InvoiceService $invoiceService
*/
public function __construct(CourseService $courseService, InvoiceService $invoiceService) {
$this->courseService = $courseService;
$this->invoiceService = $invoiceService;
}
/**
* Enroll a student to a course.
*
* #param integer $studentId
* #param integer $courseId
* #return bool Enrolled or not.
*/
public function enrollToCourse($studentId, $courseId) {
//... Use here the CourseService too - for what you said regarding the enrollment.
$enrolled = $this->studentRepo->enrollToCourse($studentId, $courseId);
return $enrolled;
}
/**
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceService->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
}
class CourseService {
protected $invoiceService;
/**
* Invoice service is used to get the (financial) restrictions for a course.
*
* #param InvoiceService $invoiceService
*/
public function __construct(InvoiceService $invoiceService) {
$this->invoiceService = $invoiceService;
}
/**
* Get a course and its corresponding (financial) restrictions list.
*
* #param integer $courseId
* #return Course Course domain object.
*/
public function getCourse($courseId) {
$course = $this->courseRepo->getCourse($courseId);
$course->restrictions = $this->getRestrictionsForCourse($course->courseId);
return $course;
}
/**
* Get the (financial) restrictions for a specified course.
*
* #param integer $courseId
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseId) {
$restrictions = $this->invoiceService->getRestrictionsForCourse($courseId);
return $restrictions;
}
}
Class InvoiceService {
/**
* No student service needed!
*/
public function __construct() {
//...
}
/**
* Again, no student service needed: the invoice service
* fetches by itself the needed infos from the database.
*
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceRepo->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
/**
* Get the (financial) restrictions for a course.
*
* #param integer $studentId
* #param integer $courseid
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseid) {
$restrictions = $this->invoiceRepo->getRestrictionsForCourse($courseid);
return isset($restrictions) ? $restrictions : [];
}
/*
* Quote: "Some processing to do to the raw
* db records that are fetched from the InvoiceRepo".
*/
//...
}
I would change your invoiceService to not depend on student service. Pass in what you need to the invoiceService. The logic of what to do with those student details can stay in invoiceService, but the content can be passed in.
Does Laravel allow us to add multiple Policies for a Model? I.e. consider App\Providers\ASuthServiceProvider's $policies property:
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Team' => 'App\Policies\RoundPolicy',
'App\Team' => 'App\Policies\AnotherPolicy',
];
I haven't tested it in an application, because even if it worked, I would be here asking a similar question, regarding whether this is considered bad practise or prone to unexpected behaviour.
The alternative I have is a very messy Policy, containing policies relating to several controllers, named in camel case:
/**
* Allows coach of Team and admin to see the Team management view.
* Used in TeamManagementController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function manage(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows a coach to detach themself from a Team.
* Used in TeamController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function detach(User $user, Team $team)
{
return $user->id === $team->user_id;
}
/**
* Below function are used for controllers other than TeamController and TeamManagementController.
* Reason: We need to authorize, based on a Team. Hence, using this Policy.
*/
/**
* Allows coach of Team, as well as admin to view players of a Team.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function indexPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows coach of Team, as well as admin to view players of a Team as an array.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function fetchPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
etc. etc.
You could use traits to separate the logic for your policy.
You would create a base TeamPolicy and then multiple traits with the various methods that you would want within the base class.
<?php
class TeamPolicy
{
use RoundPolicy, AnotherPolicy;
}
The $policies variable uses the model as key and as value a policy. Keys are unique so you can only set one policy per model. However you can use a policy on multiple models.
In your case the App\Policies\AnotherPolicy is the only one which will be used. Also assigning multiple models the same policy really depends on what you want to do. Basically you do not want messy or gross code. So if you create a policy for two models and the policy code becomes too large, it is time to consider if creating another policy would make the code simpler/less gross.
You need to create a model class that will be associated with the policy.
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Round' => 'App\Policies\RoundPolicy',
'App\Another' => 'App\Policies\AnotherPolicy',
];
Create model classes which extend the Team class. The advantage of this approach is to have separate relationships and functions for respective business logic.
namespace App\Models;
class Round extend Team
I've got this "500 Internal Server Error - LogicException: Unable to guess how to get a Doctrine instance from the request information".
Here is my controller's action definition:
/**
* #Route("/gatherplayer/{player_name}/{gather_id}")
* #Template()
*/
public function createAction(Player $player, Gather $gather)
{
// ...
}
And it doesn't work, probably because Doctrine 2 can not "guess"... So how do I make Doctrine 2 guess, and well?
The Doctrine doesn't know how to use request parameters in order to query entities specified in the function's signature.
You will need to help it by specifying some mapping information:
/**
* #Route("/gatherplayer/{player_name}/{gather_id}")
*
* #ParamConverter("player", options={"mapping": {"player_name" : "name"}})
* #ParamConverter("gather", options={"mapping": {"gather_id" : "id"}})
*
* #Template()
*/
public function createAction(Player $player, Gather $gather)
{
// ...
}
/**
* #Route("/gatherplayer/{name}/{id}")
* #Template()
*/
public function createAction(Player $player, Gather $gather)
I didn't find any help in paramconverter's (poor?) documentation, since it doesn't describe how it works, how it guesses with more than one parameters and stuff. Plus I'm not sure it's needed since what I just wrote works properly.
My mystake was not to use the name of my attributs so doctrine couldn't guess right. I changed {player_name} to {name} and {gather_id} to {id}.
Then I changed the names of my id in their entities from "id" to "id_gather" and "id_player" so I'm now able to do that :
/**
* #Route("/gatherplayer/{id_player}/{id_gather}")
* #Template()
*/
public function createAction(Player $player, Gather $gather)
which is a lot more effective than
* #Route("/gatherplayer/{id}/{id}")
Now I'm wondering how I can make this work
/**
* #Route("/gatherplayer/{player}/{gather}")
* #Template()
*/
public function deleteAction(Gather_Player $gather_player)
try this:
/**
* #Route("/gatherplayer/{player_name}/{gather_id}")
* #ParamConverter("player", class="YourBundle:Player")
* #ParamConverter("gather", class="YourBundle:Gather")
* #Template()
*/
public function createAction(Player $player, Gather $gather)
The parameters on the signature of the #Route annotation must match the entities fields, so that Doctrine makes automatically the convertion.
Otherwise you need to do the convertion manually by using the annotation #ParamConverter as it's mentionned on the other responses.
#1ed is right, you should define a #paramConverter in order to get a Player instance or a Gather instance.