I have a simple website running Laravel Jetstream with Teams enabled. On this website, you can create different "to-do tasks", which are owned by a team. I have a model called Task.
I am trying to create a public facing API, so my users can query their tasks from their own applications. In my routes/api.php file, I have added this:
Route::middleware('auth:sanctum')->group(function(){
Route::apiResources([
'tasks' => \App\Http\Controllers\API\TaskController::class,
]);
});
And then in the TaskController, I have only begun coding the index method:
/**
* Display a listing of the resource.
* #queryParam team int The team to pull tasks for.
* #return \Illuminate\Http\Response
*/
public function index()
{
if(request()->team){
$tasks = Task::where('team_id', request()->team)->get();
return TaskResource::collection($tasks);
}
return response([
'status' => 'error',
'description' => "Missing required parameter `team`."
], 422);
}
Now this works fine. I can make a GET request to https://example.org/api/tasks?team=1 and successfully get all the tasks related to team.id = 1.
However, what if I want to include multiple query parameters - some required, others only optional. For example, if I want to let users access all tasks with a given status:
https://example.org/api/tasks?team=1&status=0
What is the best practices around this? As I can see where things are going now, I will end up with a lot of if/else statement just to check for valid parameters and given a correct error response code if something is missing.
Edit
I have changed my URL to be: https://example.org/api/teams/{team}/tasks - so now the team must be added to the URL. However, I am not sure how to add filters with Spatie's Query Builder:
public function index(Team $team)
{
$tasks = QueryBuilder::for($team)
->with('tasks')
->allowedFilters('tasks.status')
->toSql();
dd($tasks);
}
So the above simply just prints:
"select * from `teams`"
How can I select the relationship tasks from team, with filters?
The right way
The advanced solution, i have built a handful of custom ways of handling search query parameters. Which is what you basically wants to do, the best solution by far is the spatie package Query Builder.
QueryBuilder::for(Task::class)
->allowedFilters(['status', 'team_id'])
->get();
This operation will do the same as you want to do, and you can filter it like so.
?fields[status]=1
In my experience making team_id searchable by team and similar cases is not worth it, just have it one to one between columns and input. The package has rich opportunities for special cases and customization.
The simple way
Something as straight forward like your problem, does not need a package off course. It is just convenient and avoids you writing some boiler plate code.
This is a fairly simple problem where you have a query parameter and a column you need to search in. This can be represented with an array where the $key being the query parameter and $value being the column.
$searchable = [
'team' => 'team_id',
'status' => 'status',
];
Instead of doing a bunch of if statements you can simplify it. Checking if the request has your $searchables and if act upon it.
$request = resolve(Request::class);
$query = Task::query();
foreach ($this->seachables as $key => $value) {
if ($query->query->has($key)) {
$query->where($value, $query->query->get($key))
}
}
$tasks = $query->get();
This is a fairly basic example and here comes the problem not going with a package. You have to consider how to handle handle like queries, relationship queries etc.
In my experiences extending $value to an array or including closures to change the way the logic on the query builder works can be an option. This is thou the short coming of the simple solution.
Wrap up
Here you have two solutions where actually both are correct, find what suits your needs and see what you can use. This is a fairly hard problem to handle pragmatic, as the simply way often gets degraded as more an more explicit search functionality has to be implemented. On the other side using a package can be overkill, provide weird dependencies and also force you into a certain design approach. Pick your poison and hope at least this provides some guidance that can lead you in the right direction.
Related
I am using form requests in Laravel for validation. I have noticed a pattern that emerges all the time and I couldn't find a solution for it on SE (or at least googling it didn't help me).
Let's say we are creating an API and we are using Laravel's apiResource to create the usual CRUD methods: store, update and delete
Obviously, when we are storing a new record, the field id is not required, but the rest of the fields might be required (and in most cases are). But when we are updating a record, we face the opposite situation. id is required while other fields are no longer required.
Is it possible to handle this situation with one form request in Laravel? Can we use Laravel's required_if in an intelligent way to avoid code duplication?
Edit: it doesn't have to be necessarily a Laravel solution. A solution that uses PHP would be fine too (as long as it is clean and follows SOLID principles).
I faced this problem lots of times and I understand your frustration...
From my point of view and professional experience, the best solution was all the time to have specific FormRequests for each case:
One for store with its own rules
Other for update with similar rules but not same as store
And last one for delete (for sure way less rules than the others and no duplication)
I know you said "no code duplication", but as it is right now, that is not possible (but you should not have code duplication as I stated before).
You said "as long as it is clean and follows SOLID principles", remember SOLID, S = Single Responsability, so if you want to solve this issue with a single FormRequest you are already breaking S. I cannot image a FormRequest with 10 or 15 inputs and those depends on if it is store, update, or delete. That is going to not be clean and for sure will not follow SOLID principles.
What about checking the method and then returning a set of rules based on that method? For instance, I have an InvoiceFormRequest with the following rules. So I'm able to use one form request for two different methods.
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
if ($this->isMethod('post')) {
return [
'template' => 'required',
'due_by_date' => 'required',
'description' => 'required',
'charge_items' => 'required',
];
}
if ($this->isMethod('put')) {
return [
'due_by_date' => 'required',
'description' => 'required',
'charge_items' => 'required',
];
}
}
Here are two possible solutions that I came up with
Use the controller method for returning the proper validation rules:
public function rules()
{
$method = $this->route()->getActionMethod();
switch($method){
case 'store':
return [
\\ validation rules
]
...
}
}
Use $this->getMethod() instead of $this->route()->getActionMethod() and validate by HTTP methods instead.
You could also store your validation rules in an array and manipulate it to reduce code duplication.
This resolves the issue of code duplication to a good extent, I think.
First the actual state: There is a ZF2 application with a form. The form contains some autocomplete fields (implemented on the frontend side with jQuery Autocomplete).
The SQL statements behind it look like this:
SELECT name FROM countries WHERE countries.name LIKE %en%
-> should find "Arg[en]tina", "Arm[en]ia", "B[en]in", "Turkm[en]istan" etc.
or
SELECT name FROM countries WHERE countries.continent_id = 2
-> should find "Afghanistan", "Armenia", "Azerbaijan" etc. (2 = Asia)
or
SELECT name FROM countries WHERE countries.continent_id = 2 AND countries.name LIKE %en%
-> should find "Arm[en]ia", "Turkm[en]istan" etc. (2 = Asia)
Of course, it leads to the problem, that the database gets terrorized by a lot of small autocomplete requests. Caching should help -- and I've already started implementing a Zend\Cache\Storage\Adapter\Apcu-based caching mechanism. But then I saw the next trouble: Using a common cache like APCu I cannot filter the results dynamically. So such a cache seems not to work for a case with autocomplete.
I'm pretty sure, that it's a common issue and there is already a solution for this.
How to realize a caching mechanism in a ZF2 application for the autocomplete functionality?
There is nothing to do with the ZF2 here. This is all about a custom search service design and it's challenges.
Without a proper caching layer and/or full-text search engine, building an autocomplete implementation such this would be suicide for the application. You can easily achieve tens of thousands of unnecessarily repeated queries in a very short time.
In an "ideal" world, a good autocomplete implementation utilizes a full-text search engine under the hood such as Elasticsearch or Apache Solr. And uses their Completion Suggester and Suggester components respectively.
Anyway a simple autocompletion feature still achievable using only an object cache and database. Only you need is a helper method to create a proper "cache key" for the every letter combinations. For example:
function createKeyByQuery($str)
{
return 'autocomplete-prefix-'.(string) $str;
}
and in your suggest() method:
public function suggest($keyword)
{
$key = $this->createKeyByQuery($keyword);
if($this->cache->hasItem($key)) {
return $this->cache->getItem($key);
}
// fetch form the database here
$data = $this->db->query();
$this->cache->setItem($key, $data);
return $data;
}
If the count of your filters are not too much, just make them part of the key too. In this scenario, signature of the suggest method would be:
public function suggest($keyword, array $filters = []);
and key generator needs an update:
function createKeyByQuery($str, array $filters = [])
{
return 'autocomplete-prefix-' . (string) $str . md5(serialize($filters));
}
This solution may not appropriate for the complicated/domain related data because it has a pretty big invalidation challenge. Eg. how you would find the cache keys which holds the "Argentina" in the payload?
Since you're dealing only with the list of countries and continents as filter, it should solve the problem.
For the england keyword and two different filter with total of 10 filter options, there will be 10x2x7 = 140 distinct cache key(s). For a single filter with 5 option, 5x1x7 = 37 distinct keys.
APCu is a good choice for this implementation.
Hope it helps.
I'm using laravel but it's not important, when you create a controller with laravel command line tool, it puts 4 default function in there for create and update.
create and store for save
edit and update for well update!
This is what laravel suggest for Shop controller.
class ShopController extends Controller
{
public function create()
{
// return create view
}
public function store(Request $request)
{
// save a shop
}
public function edit($id)
{
// find a shop , return edit view
}
public function update(Request $request, $id)
{
// find the shop with id , update the shop
}
}
But I like to use the same methods for showing view and store/update my row and avoid writing lots of extra code.
class ShopController extends Controller
{
public function create($id = 0)
{
return view('shop-create' , ['edit'=> Shop::find($id)]);
}
public function store(Request $request , $id = 0 )
{
$whitelist = [
'title'=>'required',
'phone'=>'present|numeric' ,
'address'=>'present' ,
];
$this->validate($request, $whitelist );
$shop = Shop::findOrNew($id) ;
// find a shop with given id or create a new shop instance
foreach($whitelist as $k=>$v)
$shop->$k = $request[$k];
$shop->save();
}
}
Naturally I go with what I like (second option), but since laravel suggest the first way, just out of curiosity is there any reason why I shouldn't do it like this? Is this considered bad practice in any way?
Nothing wrong, but you code will be harder to understand, IMHO.
e.g.:
What does this method do? It's called create, but it also edits?
The view is called shop-create but it also edits?
Passing a 0 parameter as default for id and trying to find it every time is unnecessary.
public function create($id = 0)
{
return view('shop-create' , ['edit'=> Shop::find($id)]);
}
Although you're thinking that you are simplifying your code, you are turning it more complicated since you are breaking the Single Responsibility principle from SOLID.
It's easier to understand if you have something like the Laravel suggestion.
Also you keep a very common pattern that any Laravel developer will understand, so you can hire someone to take care of your code and do not worry if he will understand.
There is nothing wrong with doing it your way. The "laravel" way you mention is when you create a Restful resource controller and is simply one way to tackle it.
I guess those controller methods were picked because they line up nicely to a "restful" type of controller. If you were to be building a true rest api, then how you do it becomes far more strict from a standards point of view (not imposed by laravel, but line up better to the laravel way).
If you aren't creating a public facing api, or something that is going to be consumed by external entities, then I say design your controllers that work best for you and your team
This is how i usually do it, this way you can still have different validation by using the requests and it's still clear (imo) what the functions do.
public function store(AdminPartnerRequest $request)
{
return $this->handleCreateOrUpdate($request);
}
public function update(AdminPartnerRequest $request, $id)
{
return $this->handleCreateOrUpdate($request,true, $id);
}
private function handleCreateOrUpdate($request, $edit = false, $id = null)
{
if ($edit){
$partner = Partner::find($id);
} else{
$partner = new Partner();
}
$partner->name = $request->input('name');
$partner->picture = $request->input('image');
$partner->save();
return \Redirect::route('admin.partners.index');
}
Using same function for save() and update() is good idea but at the same time it will increase complexity .. One point is If in future you want to change anything you need to change it only at one place.
But at the same time you need to take some extra care.
As your function should be more dynamic.
1) Multiple records manipulation : you may require to update more than one raws at the same time so your function should be enough flexible to insert/update single/multiple values by the same function. Meaning , single query should be fired for multiple records in both the cases.
2) Validation if value already exist : When you are going to check some validation ...
in insert case you only need to check if the value is exist in db or not
when in update case you need to check with exclusion of current id
e.g.
for insert case
$this->validate($request, [
'email' => 'required|string|email|unique:tablename,email'
]);
for update case
$this->validate($request, [
'email' => 'required|string|email|unique:tablename,email,'.$id.',id'
]);
And at last very small point but need to be considered ..
3) Success message : At the time of insertion message should be "added successfully" and at updation time Record "updated successfully"
Small project, do whatever you want. Large with other developers, follow the conventions.
Coding conventions are a set of guidelines for a specific programming language that recommend programming style, practices, and methods for each aspect of a program written in that language. These conventions usually cover file organization, indentation, comments, declarations, statements, white space, naming conventions, programming practices, programming principles, programming rules of thumb, architectural best practices, etc. These are guidelines for software structural quality. Software programmers are highly recommended to follow these guidelines to help improve the readability of their source code and make software maintenance easier. Coding conventions are only applicable to the human maintainers and peer reviewers of a software project. Conventions may be formalized in a documented set of rules that an entire team or company follows, or may be as informal as the habitual coding practices of an individual. Coding conventions are not enforced by compilers. -- https://en.wikipedia.org/wiki/Coding_conventions
I used this method in a last project of mine, we called the store() and update() function manage() instead and had a getManage() which would use the same view for creating and editing. I liked this method a lot yet came across a few things worth noting. Sadly the cons outway the pros if you ever have to face those issues :(
Pros:
Smaller code - No longer do you have duplicate lines of code in your store() and update() function.
Faster to re-use with basic models - ctrl+c ctrl+v ctrl+f ctrl+r if you know what I mean.
Easier to add/change input values - An extra value would not mean having to change store() and update() to make sure they both utilize the extra input.
One function to rule them all - As long as you are not doing anything special, you can even define one function for everything. Need to change something? You've got one function, no worries.
Cons:
Code is harder to understand for others (or an older you) - If someone is new to this method or hasn't used it in a while, understanding what happens within your function is a little harder than having two separate ones.
Validation is a nuisance - As stated in this answer validation may be different for create and update. Meaning you may sometimes have to write two validations which will eventually lead to messy code and we don't want that!
Value insertion wasn't as cool as I thought - If you want to use the same predefined array to create or update then you may run into the problem of wanting to insert values on create yet never want to update them. This eventually led to either ugly if statements or two predefined arrays.
Eventually it's up to what you're going to make and what you want to do. If you have a basic website which will manage blog posts and pages then have no worries going for a shared store() and update() function. Yet if you're creating a huge CMS with many models, relations and different create and update input values (which may mean different validation) then I'd go with what Laravel recommends. It would be much easier to maintain in the long run and you won't have to deal with headaches, hacky fixes and unclean code.
Whatever you do, don't do both in different controllers! That would be confusing.
By the way, if you're wondering what kind of project I had - it was a huge CMS. So even though it was very useful and easy in some cases, it was sadly not worth it.
Nothing wrong, but in that case you have to maintain proper comments that specify that your function perform add / edit and for that you are using some variable like $id or some thing else. If it is available than you can update the record otherwise insert it.
I'm working on an application written in PHP. I decided to follow the MVC architecture.
However, as the code gets bigger and bigger, I realized that some code gets duplicated in some cases. Also, I'm still confused whether I should use static functions when quering the database or not.
Let's take an example on how I do it :
class User {
private id;
private name;
private age;
}
Now, inside this class I will write methods that operate on a single user instance (CRUD operations). On the other hand, I added general static functions to deal with multiple users like :
public static function getUsers()
The main problem that I'm facing is that I have to access fields through the results when I need to loop through users in my views. for example :
$users = User::getUsers();
// View
foreach($users as $user) {
echo $user['firstname'];
echo $user['lastname'];
}
I decided to do this because I didn't feel it's necessary to create a single user instance for all the users just to do some simple data processing like displaying their informations. But, what if I change the table fields names ? I have to go through all the code and change those fields, and this is what bothers me.
So my question is, how do you deal with database queries like that, and is it fine to use static functions when querying the database. And finally, where is it logical to store those "displaying" functions like the one I talked about ?
Your approach seems fine, howerver I would still use caching like memcached to cache values and then you can remove static.
public function getUsers() {
$users = $cacheObj->get('all_users');
if ($users === false) {
//use your query to grab users and set it to cache
$users = "FROM QUERY";
$cacheObj->set('all_users', $users);
}
return $users;
}
(M)odel (V)iew (C)ontroller is a great choice choice, but my advice is look at using a framework. The con is they can have a step learning curve, pro is it does a lot of heavy lifting. But if you want to proceed on your own fair play, it can be tough to do it yourself.
Location wise you have a choice because the model is not clearly define:
You'll hear the term "business logic" used, basically Model has everything baring views and the controllers. The controllers should be lean only moving data then returning it to the view.
You model houses DB interaction, data conversions, timezone changes, general day to day functions.
Moudle
/User
/Model
/DB or (Entities and Mapper)
/Utils
I use Zend and it uses table gateways for standard CRUD to avoid repetition.
Where you have the getUsers() method you just pass a array to it, and it becomes really reusable and you'd just have different arrays in various controller actions and it builds the queries for you from the array info.
Example:
$data = array ('id' => 26)
$userMapper->getUsers($data);
to get user 26
enter code here
$data = array ('active' => 1, 'name' => 'Darren')
$userMapper->getUsers($data);`
to get active users named Darren
I hope this help.
Is there any open source (or example) code for Symfony2 which can filter certain model using multiple parameters? A good example of what I'm looking for can be seen on this Trulia web page.
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/0-500_price/wd,dw_amenities/sm_dogs_pets"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400-500_price/wd,dw_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/wd,dw_amenities"
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/400p_price/dw,cs_amenities
http://www.trulia.com/for_rent/Chicago,IL/#for_rent/Chicago,IL/1p_beds/1p_baths/400p_price/dw,cs_amenities
Note how URL are build when clicking in the form, I guess is using one controller for all this routes, How is it done?.
I Don't think it will be redirecting all the possible routes to a specific controller, (shown below), maybe some sort of dynamic routing?
/**
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{mix_price}-{max_price}_price /{amenities_list}
* #Route("/for_rent/{state}/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{bath}_bath/{mix_price}-{max_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{mix_price}_price/{amenities_list}
* #Route("/for_rent/{state}/{beds}_beds/{bath}_bath/{amenities_list}
* ........
*/
public function filterAction($state, $beds, $bath, $min_price, $max_price ....)
{
....
}
Thanks.
For simple queries (i.e. where you don't need to have a data range, such as min-max value), you can use the entity repository to find entities by the request parameters given. Assuming that your entity is Acme\FooBundle\Entity\Bar:
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeFooBundle:Bar');
$criteria = array(
'state' => $state,
'beds' => $beds,
// and so on...
);
$data = $repo->findBy($criteria);
When building the $criteria array, you'll probably want some logic so that you only sort by criteria that have been provided, instead of all possible values. $data will then contain all entities that match the criteria.
For more complex queries, you'll want to look into DQL (and perhaps a custom repository) for finer-grained control of the entities that you're pulling out.
To construct your routes, i'm sure you had a look at the Routing page of the documentation, but did you notice that you can put requirements on routes? This page explains how to do it with annotations.
As for the filtering, I suppose DQL would be ok, but you can also write straight up SQL with Doctrine, and map the results of your query to one or more entities. This is described here. It may be more flexible than DQL.
csg, your solution is good (with #Route("/search/{q}) if you only need to use routing in "one-way". But what if you will need to print some price filter links on page accessible by url:
http://www.trulia.com/for_sale/30000-1000000_price/10001_zip/
In case of #Route("/search/{q} you will not be able to use route method url generate with params.
There is a great Bundle called LexikFormFilterBundle "lexik/form-filter-bundle": "~2.0" that helps you generate the complex DQL after the Filter form completed by the user.
I created a Bundle, that depends on it, that changes the types of a given FormType (like the one generated by SencioGeneratorBundle) So you can display the right FilterForm and then create the DQL after it (with Lexik).
You can install it with Composer, following this README.md
All it does is override the Doctrine Type Guesser, that suggests the required FormType for each Entity field, and replace the given Type by the proper LexikFormFilterType. For instance, replaces a simple NumberType by a filter_number which renders as two numbers, Max and Min interval boundaries.
private function createFilterForm($formType)
{
$adapter = $this->get('dd_form.form_adapter');
$form = $adapter->adaptForm(
$formType,
$this->generateUrl('document_search'),
array('fieldToRemove1', 'fieldToRemove2')
);
return $form;
}
Upon form Submit, you just give it to Lexik and run the generated query, as shown in my example.
public function searchAction(Request $request)
{
// $docType = new FormType/FQCN() could do too.
$docType = 'FormType/FQCN';
$filterForm = $this->createFilterForm($docType);
$filterForm->handleRequest($request);
$filterBuilder = $this->getDocRepo($docType)
->createQueryBuilder('e');
$this->get('lexik_form_filter.query_builder_updater')
->addFilterConditions($filterForm, $filterBuilder);
$entities = $filterBuilder->getQuery()->execute();
return array(
'entities' => $entities,
'filterForm' => $filterForm->createView(),
);
}