I've been working on a bunch of projects lately with Doctrine 1.2 integrated into Zend Framework 1.11, and it has been really fun.
One of the most common methods I have implemented in my service layers are methods which returns a collection of my domain models according to a set of criteria being passed as arguments.
I've been creating them with interfaces like this:
//All books
$books = $service->getBooks();
//Books under certain categories and authored which matches the search term 'Hitchens'
$books = $service->getBooks(array(
'category' => array(1,2,3,4),
'author' => array('like' => '%Hitchens%', 'diedBefore' => Zend_Date::now()),
'orderBy' => 'bookTitle',
'limit' => 10
));
And the implementation look like this:
public function getBooks(array $options = null)
{
$query = Doctrine_Query::create()->from('LibSys_Model_Book as book');
if($options !== null){
if(isset($options['author']){
//Manipulate Doctrine_Query object here....
}
//More 'ifs' and manipulations to the Doctrine_Query object...(additional criterias, orderBy clauses, limits, offsets, etc.)
}
}
And as my the need for more criteria rises, the messier the implementations get. And needless to say, there are a lot of code re-use everywhere, and updating the code is so such a tedious task.
I've been thinking that it would be a lot better if I can pass in the criteria as objects instead of array segments, utilizing the Visitor Pattern with something like this:
$authorCriteria = new LibSys_Criteria_Author();
$authorCriteria->like('%Hitchens%');
$authorCriteria->diedBefore(Zend_Date::now());
$books = $service->getBooks(array(
$authorCriteria,
new LibSys_Criteria_Category(array(1,2,3,4))
));
With LibSys_Criteria_Category and LibSys_Criteria_Author implementing a common interface which might look like this:
interface LibSys_Doctrine_Criteria_Interface
{
public function applyCriteria(Doctrine_Query $query);
}
Therefore, it will just be a matter of looping through the set of criteria objects provided and pass along the Doctrine_Query object to them as they take turns in manipulating it to suit the needs:
public function getBooks(array $criteria = null)
{
$query = Doctrine_Query::create()->from('LibSys_Model_Book as book');
if($criteria !== null){
foreach($criteria as $criterion){
if($criterion instanceof LibSys_Doctrine_Criteria_Interface){
$criterion->applyCriteria($query);
}
}
}
}
This way not only does it make them easier to use, but also make the services extendible, the criteria objects reusable, and everything much easier to maintain.
In order to make this work robustly, though, one would need to be able to inspect the Doctrine_Query object and check any existing joins, conditions, etc. This is because it is not impossible that two totally different criteria may require same set of conditions or same set of joins. In which case, the second criteria can either simply re-use an existing join and/or adjust accordingly. Also, it wouldn't be hard to imagine cases where a criterion's set of conditions are mutually incompatible with another, in which case should should warrant an exception to thrown.
So my question is:
Is there a way to inspect a Doctrine_Query and get information regarding which components has been joined, or any set of conditions already applied to it?
Related
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.
I'm querying big chunks of data with cachephp's find. I use recursive 2. (I really need that much recursion sadly.) I want to cache the result from associations, but I don't know where to return them. For example I have a Card table and card belongs to Artist. When I query something from Card, the find method runs in the Card table, but not in the Artist table, but I get the Artist value for the Card's artist_id field and I see a query in the query log like this:
`Artist`.`id`, `Artist`.`name` FROM `swords`.`artists` AS `Artist` WHERE `Artist`.`id` = 93
My question is how can I cache this type of queries?
Thanks!
1. Where does Cake "do" this?
CakePHP does this really cool but - as you have discovered yourself - sometimes expensive operation in its different DataSource::read() Method implementations. For example in the Dbo Datasources its here. As you can see, you have no direct 'hooks' (= callbacks) at the point where Cake determines the value of the $recursive option and may decides to query your associations. BUT we have before and after callbacks.
2. Where to Cache the associated Data?
Such an operation is in my opinion best suited in the beforeFind and afterFind callback method of your Model classes OR equivalent with Model.beforeFind and Model.afterFind event listeners attached to the models event manager.
The general idea is to check your Cache in the beforeFind method. If you have some data cached, change the $recursive option to a lower value (e.g. -1, 0 or 1) and do the normal query. In the afterFind method, you merge your cached data with the newly fetched data from your database.
Note that beforeFind is only called on the Model from which you are actually fetching the data, whereas afterFind is also called on every associated Model, thus the $primary parameter.
3. An Example?
// We are in a Model.
protected $cacheKey;
public function beforeFind($query) {
if (isset($query["recursive"]) && $query["recursive"] == 2) {
$this->cacheKey = genereate_my_unique_query_cache_key($query); // Todo
if (Cache::read($this->cacheKey) !== false) {
$query["recursive"] = 0; // 1, -1, ...
return $query;
}
}
return parent::beforeFind($query);
}
public function afterFind($results, $primary = false) {
if ($primary && $this->cacheKey) {
if (($cachedData = Cache::read($this->cacheKey)) !== false) {
$results = array_merge($results, $cachedData);
// Maybe use Hash::merge() instead of array_merge
// or something completely different.
} else {
$data = ...;
// Extract your data from $results here,
// Hash::extract() is your friend!
// But use debug($results) if you have no clue :)
Cache::write($this->cacheKey, $data);
}
$this->cacheKey = null;
}
return parent::afterFind($results, $primary);
}
4. What else?
If you are having trouble with deep / high values of $recursion, have a look into Cake's Containable Behavior. This allows you to filter even the deepest recursions.
As another tip: sometimes such deep recursions can be a sign of a general bad or suboptimal design (Database Schema, general Software Architecture, Process and Functional flow of the Appliaction, and so on). Maybe there is an easier way to achieve your desired result?
The easiest way to do this is to install the CakePHP Autocache Plugin.
I've been using this (with several custom modifications) for the last 6 months, and it works extremely well. It will not only cache the recursive data as you want, but also any other model query. It can bring the number of queries per request to zero, and still be able to invalidate its cache when the data changes. It's the holy grail of caching... ad-hoc solutions aren't anywhere near as good as this plugin.
Write query result like following
Cache::write("cache_name",$result);
When you want to retrieve data from cache then write like
$results = Cache::read("cache_name");
In a restful API for fruit, the request assumes something like this:
api/fruit
api/fruit?limit=100
api/fruit/1
api/fruit?color=red
I think there must be a standard for functions that do the work. For instance, something may easily translate to fruit.class.php.
fruit.class.php
function get ($id, $params, $limit) {
// query, etc.
}
So for my above examples, the code would look like
api/fruit
$fruit = $oFruit->get();
api/fruit?limit=100
$fruit = $oFruit->get(NULL, NULL, 100);
api/fruit/1
$fruit = $oFruit->get(1);
api/fruit?color=red
$fruit = $oFruit->get(NULL, array('color' => 'red'));
Is there an industry standard like this or are API/database functions always a mess? I’d really like to standardize my functions.
No, there is no standard as others have already answered...however, in answer to your issue about creating too many methods...I usually only have a single search() method that returns 1 or more results based on my search criteria. I usually create a "search object" that contains my where conditions in an OOP fashion that can then be parsed by the data layer...but that's probably more than you want to get into...many people would build their DQL for their data-layer, etc. There are a lot of ways to avoid getById, getByColor, getByTexture, getByColorAndTexture. If you start seeing lots of methods to cover every single possible combination of search, then you're probably doing it wrong.
As to rest method naming...ZF2 is not the answer, but it's the one I"m currently using on my project at work, and its methods are laid out like so (please note, this is HORRIBLE, dangerous code...open to SQL injection...just doing it for example):
// for GET URL: /api/fruit?color=red
public function getList() {
$where = array();
if( isset($_GET['color']) ) {
$where[] = "color='{$_GET['color']}'";
}
if( isset($_GET['texture']) ) {
$where[] = "texture='{$_GET['texture']}'";
}
return search( implode(' AND ',$where) );
}
// for GET URL: /api/fruit/3
public function get( $id ) {
return getById( $id );
}
// for POST URL /api/fruit
public function create( $postArray ) {
$fruit = new Fruit();
$fruit->color = $postArray['color'];
save($fruit);
}
// for PUT URL /api/fruit/3
public function update( $id, $putArray ) {
$fruit = getById($id);
$fruit->color = $putArray['color'];
save($fruit);
}
// for DELETE /api/fruit/3
public function delete( $id ) {
$fruit = getById($id);
delete($fruit);
}
Prelude
There isn't really a standard or convention for how urls should look, that cover (nearly) all cases.
The only standard I can think of is HATEOAS (Hypermedia as the Engine of Application State), which basically states that a client should derive the url's it can use from previous requests. This means that what the urls are isn't really important (but how the client can discover them is).
REST is kind of a hype nowadays, and it's often not understood what it actually is. I suggest your read Roy Fielding's dissertation on Architectural Styles and the Design of Network-based Software Architectures, especially chapter 5.
Richardson Maturity Model is also a good read.
PS: HAL (Hypertext Application Language) is a standard (among others) for implementing HATEOAS.
Interface
Because there is no standard on urls, there is also no standard for interfaces on that subject. It highly depends on your requirements, your taste, and perhaps what framework you're building the application with.
David Sadowski has made a nice list of libraries and frameworks that can help you develop RESTfull applications. I suggest you take a look at a couple of them, to see if and how they solve the problems you encounter. You should be able to get some idea's from them.
Also read the references I made in the prelude, as it will give you good insight on do's and don'ts of building real RESTfull applications.
PS: Sorry for not giving you a straightforward definitive answer! (I don't think one really exists.)
You are talking about get filters. I prefer magento like filters
There are no convention about how your internal code should look and not so many php frameworks support such functionality as get filters out of the box. You can have a look on magento rest api realization.
You can use functions calls like $model->get($where, $order, $limit)
but you should
define resourse properties
map resource properties to DB result fields
define which filters resource supports
check for filters, remove unsupported and build corresponding $where, $order, $limit
The open source library phprs may meet your needs.
With phprs, you can implement fruit.class like this:
/**
* #path("/api/fruit")
*/
class Fruit{
/**
* #route({"GET","/"})
* #param({"color","$._GET.color"})
* #param({"limit","$._GET.limit"})
*/
function getFruits($color,$limit){
return $oFruit->get(NULL, array('color' =>$color),$limit);
}
/**
* #route({"GET","/*"})
* #param({"id","$.path[2]"})
*/
function getFruitById($id){
return $oFruit->get($id);
}
}
I'm creating a portfolio page in Codeigniter, and I'm also equipping the site with a simple CMS.
I have a controller that creates portfolio items as follows:
public function create_portfolio()
{
$this->load->model('portfolio_model');
$this->portfolio_model->insert(
$this->input->post('title'),
$this->input->post('short_description'),
$this->input->post('complete_description'),
$this->input->post('github'),
$this->input->post('url')
);
}
An object-oriented approach would be something like this:
public function create_portfolio()
{
$this->confirm_login();
$this->load->model('portfolio_model');
$portfolio = new Portfolio(
$this->input->post('title'),
$this->input->post('short_description'),
$this->input->post('complete_description'),
$this->input->post('github'),
$this->input->post('url')
);
$this->portfolio_model->insert($portfolio);
}
With employers increasingly seeking those with OOP skills, I'm trying to evaluate whether the latter approach is effective. In the end, the model that I have would end up accessing all of the instance variables from the Portfolio object in order to insert into the database (I am not using an ORM).
Is there any point to actually grouping all of the input fields into an object before passing it onto the model? I may be phrasing this question the wrong way, but I would love your input.
Since your are passing values to a constructor, the constructor may do validation on the input to ensure it is valid or do some sort of other processing where
$portfolio->title may not equal $this->input->post('title');
So option 2 would be better.
I am looking for a way in Yii to re-sort a CActiveDataProvider.
Currently I am using the the data provider to fetch sorted and paginated data.
I am then looping through it with a foreach loop adjusting data for specific fields, and now I need to re-sort it based upon the new adjusted data.
I can't sort the data in the model using afterFind because I need to query another DB (MySQL) to work out a calculated value and it doesn't seem to like the switching during the processing.
I don't want to use an CArrayDataProvider because there is no obvious way to paginate the data which comes back unless I put controls into a loop while adjusting the data, however I don't know how much data could come back, say 200 records, but the only adjusting 20 for the display seems counterintuitive.
This all is then pushed to a CGridView widget.
so now I need to readjust on the array below in ascending order for example.
$dataProvider = new CActiveDataProvider( blah );
foreach ( $dataProvider->getData() as $data ) {
$data->Score += SomeModel::model()->findByPk(1)->NewScore;
}
array(
'Score' => 7
),
array(
'Score' => 6
)
$this->render('blah', array('data' => $dataProvider);
It sounds like you should simply implement a new CDataProvider of your own that wraps an existing CDataProvider. You will need to override several methods, but for most of them you can simply forward to the wrapped data provider. The code might end up looking something like this:
$dataProvider = new CActiveDataProvider(...);
$myProvider = new CustomDataProvider($dataProvider);
// you could make CustomDataProvider work like this:
$myProvider->dataMutator = function(&$row, $key) {
$row->Score = SomeModel::model()->findByPk(1)->NewScore;
};
$this->render('blah', array('data' => $myProvider);
CustomDataProvider::fetchData might look like this:
protected function fetchData()
{
$data = $this->wrappedProvider->fetchData();
array_walk(&$data, $this->dataMutator);
uasort($data, function($a, $b) { return $a->Score - $b->Score; });
return $data;
}
I 've hardwired the sort here -- this works for prototyping, but to specify the post-processing sort cleanly is not trivial because:
CDataProvider exposes a sort property which is ignored. Integrating that property fully would either be non-trivial work (you have to write code that respects multiple sort criteria etc) or change the semantics of CustomDataProvider.sort compared to sort on its base class. CDataProvider is married to CSort, so if you wanted to go for the cleanest solution from an OO perspective it would be best to implement IDataProvider from scratch.
It's not intuitive to the user of CustomDataProvider how sorting works if a single 'sortproperty is used, becausesort` cannot reflect both what the wrapped data provider will do and what the "post-processing" sort will do at the same time; however, both of the above will affect the end result.
I suggest that you get a prototype working and then have a good think about what the object model should be.