Conditionally building an Eloquent query - php

The Context
I'm using Laravel's Eloquent as my ORM. I am creating an API endpoint which provides access to Cars which have several attributes (color, make, status).
My endpoint allows clients to filter the return value by any subset of those attributes, if they provide no attributes then I will return everything.
The Question
I want to build a conditional query, which starts from "all" and narrows down based on which parameters have been specified. Here's what I've written:
public function getCars(Request $request)
{
$results = Cars::all();
if($request->has('color'))
$results = $results->where('color', $request->input('color'));
if($request->has('make'))
$results = $results->where('make', $request->input('make'));
if($request->has('status'))
$results = $results->where('status', $request->input('status'));
return $results->toJson();
}
If I call this with no parameters the API returns a list of all cars in the database.
If, however, I specify (for instance) status of 0 the API returns an empty set, despite the fact that some cars have status of 0.
Am I approaching this incorrectly? Is there something fundamental I'm missing?
Note that if instead I write:
$results = Cars::where('status', 0);
return $results->get();
The list of cars is properly generated

You should change your function like this:
public function getCars(Request $request)
{
$results = Cars::query();
if($request->has('color'))
$results = $results->where('color', $request->input('color'));
if($request->has('make'))
$results = $results->where('make', $request->input('make'));
if($request->has('status'))
$results = $results->where('status', $request->input('status'));
return $results->get()->toJson();
}

You could try this, for simplicity.
$query = Cars::query(); // no query executed, just give us a builder
$query->where(array_only($request->all(), ['color', 'make', 'status'])); // where can take a key value array to use
// update: only taking the vars you need, never trust incoming data
return $query->get(); // will be converted to Json for you
This only queries the DB for what you need. Yours is returning all results then filtering through them in a collection.
Update:
As Joseph stated, there is different functionality between $request->only() and array_only. The functionality of array_only is wanted here.

Related

PHP OOP - Return results if not calling another method with it $obj->get() and $obj->get()->count()

It's hard to explain what I want exactly but I've gotta try to...
Laravel Eloquent inspired me to write a simple php class to work with databse.
As we know We can do this in laravel:
$run = DB::table('users')->where('id', 3)->where('level', 2)->get();
Also we do that:
$run = DB::table('users')->where('id', 3)->where('level', 2)->get()->count();
Also we can do that:
$run = DB::table('users')->where('id', 3)->where('level', 2)->get()->first();
Even we can do that too:
$run = DB::table('users')->where('id', 3)->where('level', 2)->get()->pluck('id')->toArray();
And that I have not ever tried but I believe it works too:
$run = DB::table('users')->where('id', 3)->where('level', 2)->get()->pluck('id')->toArray()->first();
The question is "How does it work?"
How should I write to return suitable results in any of their ways?
// It was easy to write my code to return total results if I write like that
$run = DB::from('users')->where('id', 3)->where('level', 2)->get()->count();
// Or to return first result if I write like that
$run = DB::from('users')->where('id', 3)->where('level', 2)->get()->first();
// But what sould I do to return all the results if write like that (As eloquent works).
$run = DB::from('users')->where('id', 3)->where('level', 2)->get();
I need something like "if - else case for methods" like:
function __construct() {
if(if aint`t no calling any methods except **get()** ){
// Lets return default method
return $this->results();
}
else{
// Do whatever...
}
}
There is my whole code:
https://github.com/amirandev/PHP-OOP-DB-CLASS/blob/main/db.php
As I know, when you are trying something like that
$run = DB::from('users')->get()->count();
You get all users and php/laravel count users, meaning
$users = DB::from('users')->get(); // get all users
$usersConut = $users->count(); //count them away of database/mysql
The same thing with first()
when you are using this code DB::from('users')->count(); you are actually asking MySql for count not counting them in the backend.
I highly recommend using this package barryvdh/laravel-debugbar to help you see the database queris.
Each method returns $this. So the next method will have the class with the modifications done by the previous method. It's quite easy to achieve that.
class Example
{
private string $sentence = '';
public function make()
{
return $this->start()->end();
}
public function start()
{
$this->sentence .= 'This will be a ';
return $this;
}
public function end()
{
$this->sentence .= 'whole sentence.';
}
}
BTW, Eloquent query builder converts the method chain into an SQL query string. That's why it works really fast. If you just query one table, let's say users, via the query builder, and then filter the results in your application (like it was based on the where condition), the process will be quite slow because the hard work will be done by your app.
SQL is extremely fast, that's why we want to complete as many tasks as possible on the SQL side.

Is there a way to hydrate objects without looping through result sets twice?

If I use a Hydrator to put the values of my query results into an instance of my Model I'm going to have to loop through the results twice (once to hydrate each row/object and then again when I actually use the results).
I know there are various PDO_FETCH_* modes such as FETCH_INTO and FETCH_CLASS which seem to achieve the same functionality. The only difference I can see is the potential to manipulate the data during hydration.
I'm just trying to figure out the reasoning behind using hydrators when they require an additional iteration through the result set. I feel there has to be some reason or method to make that additional iteration acceptable and I'm curious what it is. I'm not sure why a programming pattern like hydrators is so populate because on the surface it seems like a waste in most situations. Is manipulating the data during hydration the only reason to use Hydrators?
"Find All" method from mapper:
($this->hydrator is an instance of Zend\Stdlib\Hydrator\ArraySerializable, but can be replaced with any other hydrator for the purpose of this question.)
/*
* #return array|PagesInterface[]
*/
public function findAll(){
//Perform select
$sql = "SELECT * FROM CMSMAIN";
$stmt = $this->dbal->prepare($sql);
$result = $stmt->execute();
if($result === true){
//$stmt->setFetchMode(\PDO::FETCH_CLASS, $modelGoesHere); //Using FETCH_CLASS populates the model directly eliminating the need for the extra iteration.
$records = $stmt->fetchAll();
$rows = array();
foreach($records as $value){ //First Loop to hydrate
$rows[] = $this->hydrator->hydrate($value, new PagesModel($this->logger));
}
return $rows;
}
return array();
}

Create a find method with PDO?

In the past I've worked with framework as Slim or CodeIgniter, both provide method such as getWhere(), this method return true or false if the array content passed to the getWhere was found on database table.
Actually I've created my own layer class that extends PDO functionality, my goal is create a method that take care to look for a specific database content based on the supplied parameters, currently I created this:
public function findRecordById($table, $where = null, $param = null)
{
$results = $this->select("SELECT * FROM $table WHERE $where",$param);
if(count($results) == 0)
{
return false;
}
return true;
}
for search a content I simply do:
if(!$this->db->findRecordById("table_name", "code = :key AND param2 = :code",
array("key" => $arr['key'], "code" => $arr['code']))){
echo "content not found";
}
now all working pretty well but I think that the call on the condition is a bit 'too long and impractical, I would like to optimize everything maybe going all the content into an array or something, but until now I have a precise idea. Some help?
I don't quite understand your question, but for the code provided I could tell that such a method should never belong to a DB wrapper, but to a CRUD class, which is a completely different story.
If you want to use such a method, it should be a method of a Model class, and used like this
$article = Article::find($id);
While for a database wrapper I would strongly suggest you to keep with raw SQL
$sql = "SELECT * FROM table_name WHERE code = :key AND param2 = :code";
$data = $this->db->query($sql, $arr)->fetchAll();
is the clean, tidy, and readable code which anyone will be able to read and understand.
As a bonus, you will be able to order the query results using ODER BY operator.

How can I take a big amount of GET parameters and filter a query depending on them cleanly?

I have this controller for a RESTful API I am building in Laravel Lumen which takes a relatively big amount of parameters and parses them into where queries, and data is fetched depending on if they were provided. For example,
GET /nodes?region=California
GET /nodes?ip=127.0.0.1
I am currently taking them in the constructor, building an array of the parameters (since I couldn't figure out how to get the raw get array in Lumen and it would be inconvenient because I already have other parameters there), and filtering out the null values (I am setting values to null if they are not in the query).
Now, when it comes to filtering the values each in the array, I am doing it by a foreach array. This is the cleanest way I could figure out to do it, without too much code (I don't want to make my controllers too fat.).
Is there any other way to do this cleanly, maybe with separation of functions/classes?
Here is my constructor code:
/**
* Get some values before using functions.
*
* #param Request $request Instance of request.
*/
public function __construct(Request $request)
{
$this->offset = (int) $request->input('offset', 0);
// TODO: I'm not sure how to implement this, code in question
$this->filters = [
'region' => $request->input('region', null),
'name' => $request->input('name', null),
'ip' => $request->input('ip', null)
];
$this->filters = array_filter($this->filters, function ($v) {
return !is_null($v);
});
// Set a sane SQL limit.
$this->limit = 5;
$this->request = $request;
}
And the controller code:
/**
* List all nodes.
*
* #return [string] [JSON containing list of nodes, if sorted.]
*/
public function all()
{
try {
// use filters provided
$data = Nodes::limit($this->limit)->offset($this->offset);
foreach ($this->filters as $filter => $value) {
$data->where($filter, $value);
}
$data = $data->get();
$response = $this->respond($data);
} catch (\Exception $e) {
$response = $this->respondServerError('Could not retrieve data from database.');
}
return $response;
}
So any time I have to do filtering of a resource-list in an API, here's how I do it.
First off though, before I begin, a quick tip concerning getting the Request object when you're in your controller method: If you add Request $request as a parameter for your all() function, you will have access to the $request variable there, same as your constructor. So the complete signature would be public function all(Request $request). Controller methods have the same magic dependency injection that other class constructors get in Laravel/Lumen. Alternatively, in your function you can always ask the app() function to give you an object of a specific class. Because the Request object is bound in the Container to just 'request', you can ask for the full class name, or just 'request': $request = app('request');
So once I have my request object, inside my controller method I like to go through each filter either as a group, or one-by-one, depending on how complex each filter is. Sometimes filters are complex, like a list of comma-separated IDs that need to be exploded into an array. If it's just simple string filters though, I tend to throw the list into an array and run through that.
Here's an example function to illustrate some ideas:
public function getIndex(Request $request)
{
//Create a User object to append WHERE clauses onto
$user = app('App\Models\User');
//Run through our simple text fields
foreach(['first_name', 'last_name', 'region', 'ip'] as $field) {
if ($request->has($field)) {
$user->where($field, $request->input($field));
}
}
//This field uses a LIKE match, handle it separately
if ($request->has('email')) {
$user->where('email', LIKE, '%' . $request->input('email') . '%');
}
//This field is a list of IDs
if ($request->has('id')) {
$ids = explode(',', $request->input('id'));
$user->whereIn('id', $ids);
}
//Use pagination
$users = $user->paginate(25);
/**
* Continue with the rest of response formatting below here
*/
}
You'll notice I used the paginate function to limit my results. When building an API endpoint that lists resources, you're going to want to put in your headers (my preference) or the response body information on how to get the first, previous, next, and last page of results. The Pagination feature in Laravel makes that easy, as it can construct most of the links using the links() method.
Unfortunately, you need to tell it what filter parameters were passed in the request so it can make sure it adds those to the links it generates. Otherwise you'll get links back without your filters, which doesn't do the client very much good for paging.
So here's a more complete example of recording filter parameters so they can be appended onto pagination links:
public function getIndex(Request $request)
{
//Create a User object to append WHERE clauses onto
$user = app('App\Models\User');
//List of filters we found to append to links later
$appends = [];
//Run through our simple text fields
foreach(['first_name', 'last_name', 'region', 'ip'] as $field) {
if ($request->has($field)) {
$appends[$field] = $request->input($field);
$user->where($field, $request->input($field));
}
}
//This field uses a LIKE match, handle it separately
if ($request->has('email')) {
$appends['email'] = $request->input('email');
$user->where('email', LIKE, '%' . $request->input('email') . '%');
}
//This field is a list of IDs
if ($request->has('id')) {
$appends['id'] = $request->input('id');
$ids = explode(',', $request->input('id'));
$user->whereIn('id', $ids);
}
//Use pagination
$users = $user->paginate(25);
//Make sure we append our filter parameters onto the pagination object
$users->appends($appends);
//Now calling $users->links() will return the correct links with the right filter info
/**
* Continue with the rest of response formatting below here
*/
}
Pagination documentation can be found here: https://laravel.com/docs/5.2/pagination
For an example of how pagination linking can be awesomely done, check out Github's API documentation: https://developer.github.com/v3/#pagination
In the end it's not too far off from what you were doing, conceptually. The advantage here is that you move the code into the method that needs it, instead of having it run in your constructor every single time the controller is initialized, even if a different method will be called.
Hope that helps!

Custommize get column from findAll function Yii

I have some problem that, I am using criteria to customize a number column query
$criteria=new CDbCriteria();
$criteria->select =array('CompanyName', 'CompanyCountCoupon','CompanyDes', 'CompanyLogo');
$models = Company::model()->findAll($criteria);
After I put it to array and echo result
$rows = array();
foreach($models as $i=>$model1) {
$rows[$i] = $model1->attributes;
}
echo CJSON::encode($rows)
My problem is that the results contains all attributes of table, and attributes not in criteria->select will set = null
{"CompanyName":"abc","CompanyCountCoupon":"0","CompanyDes":"Hello","CompanyLogo":"\/upload\/company\/abc.jpg",**"CompanyID":null,"CompanyWebSite":null,"CompanyAdrress1":null,"CompanyAdrress2":null,"CompanyPhone1":null,"CompanyPhone2":null**}
Please help me.
Thanks to all
if you go with findAll() (using ActiveRecord) you won't be able to control that part, the way to go is a custom query :
$results = Yii::app()->db->createCommand()
->select('CompanyName ,CompanyCountCoupon ,CompanyDes ,CompanyLogo')
->from('company')
//->where() // where part
->queryAll();
echo CJSON::encode($results);
now its already good to be JSON encoded and also much faster than regular ActiveRecord
Use getAttributes()
Example
$rows = Company::model()->getAttributes(array('CompanyName','CompanyCountCoupon','CompanyDes', 'CompanyLogo'));
echo CJSON::encode($rows);
This is correct behaviour.
You are asking for specific columns, so this is being correctly provided.
Recall that the attributes is part of the model, not the query.
$model = Company::model()->findByPK();
print_r($model);
...
/* Company points to the TABLE. not the query */
class Company extends CActiveRecord
{
---
}

Categories