I'm trying to implement a Results class that processes queries. So, simply put, you would have functions like this:
function all();
function first();
function paginate(int $perPage, int $pageNo = 1);
This works pretty well, the problem being that the IDE has no possible way of knowing the return type when this same results class is being used in multiple different query classes. Example:
UserQuery->results()->all() will return an array of User entities.
UserQuery->results()->first() will return a single User entity.
In some languages, you have generics, which means I could just use Results<User> in the UserQuery class and then my Results class could return T[] and T respectively.
One idea I had was to pass an empty entity as the constructor to the Results class and then try to use that property as the return type, but I couldn't figure this out. Is there any workaround to this? The main problem I'm trying to solve is IDE autocompletion and analysis, so a pure phpDoc solution is perfectly fine for my use case.
The only other workaround I can come up with is having to write a separate Results class for each entity type, which would prove to be exhausting.
I don't think there's a way to do exactly what you described, but in similar cases I would suggest using a proxy class for each Results type and document proper return types using phpDocumentor's #method. This solution has added value of having a great place for any type specific Results modifications and expansions. Here's an example:
abstract class Results
{
function all(): array
{
}
function first()
{
}
function paginate(int $perPage, int $pageNo = 1): array
{
}
}
class User { }
/**
* #method User[] all()
* #method User first()
* #method User[] paginate(int $perPage, int $pageNo = 1)
*/
class UserResults extends Results { }
class UserQuery
{
/**
* #var UserResults
*/
private $results;
public function __construct()
{
$this->results = new UserResults();
}
public function results(): UserResults
{
return $this->results;
}
}
$userQuery = new UserQuery();
$test = $userQuery->results()->all();
Related
I have this set of entities that we call nomenclators, which basically have an id field and a text-based field. The CRUD operations for these entities are virtually the same, just that in some of them the text field is called state while in others is area... and so on.
Given that, I created this base Controller
class NomenclatorsController extends Controller
{
use ValidatorTrait;
protected function deleteENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$spService = $this->get('spam_helper');
$resp = $spService->deleteEntitySpam("AplicacionBaseBundle:$entityName", $id);
if ($resp == false)
return new JsonResponse("error.$entityName.stillreferenced", Response::HTTP_FORBIDDEN);
return new JsonResponse('', Response::HTTP_ACCEPTED);
}
protected function listENTITYAction(Request $req, $entityName)
{
$size = $req->query->get('limit');
$page = $req->query->get('page');
$spService = $this->get('spam_helper');
$objectResp = $spService->allSpam("AplicacionBaseBundle:$entityName", $size, $page);
$arrayResp = $spService->spamsToArray($objectResp);
return new JsonResponse($arrayResp, Response::HTTP_ACCEPTED);
}
protected function updateENTITYAction(Request $req, $entityName)
{
$id = $req->request->get('id');
$entity = null;
if (is_numeric($id)) {
$entity = $this->getDoctrine()->getRepository("AplicacionBaseBundle:$entityName")->find($id);
} else if (!is_numeric($id) || $id == null) {
//here comes the evil
eval('$entity=new \\AplicacionBaseBundle\\Entity\\' . $entityName . '();');
$entity->setEliminado(false);
$entity->setEmpresa($this->getUser()->getEmpresa());
}
$this->populateEntity($req->request, $entity);
$errors = $this->validate($entity);
if ($errors)
return new Response(json_encode($errors), Response::HTTP_BAD_REQUEST);
$spamService = $this->get('spam_helper');
$spamService->saveEntitySpam($entity);
}
//Override in children
protected function populateEntity($req, $entity)
{
}
}
So, each time I need to write a controller for one of these nomenclators I extend this NomenclatorsController and works like a charm.
The thing is in the updateENTITYAction I use eval for dynamic instantiation as you can see, but given all I have readed about how bad is eval I am confused now, and even when there is no user interaction in my case I want to know if there is a better way of doing this than eval and if there is any noticiable performance issue when using eval like this.
By the way I am working in a web json api with symfony and extend.js, which means no view is generated in the server,my controllers match a route and receive a sort of request params and do the work.
I've done something similar in the past. Since you are extending a base class using specific classes for each entity you can instance your entity from the controller that extends NomenclatorsController.
If one of your entities is called Foo you will have a FooController that extends NomenclatorsController. Just overwrite updateENTITYAction and pass back needed variables.
An example:
<?php
use AplicacionBaseBundle\Entity\Foo as Item;
class FooController extends NomenclatorsController
{
/**
* Displays a form to edit an existing item entity.
*
* #Route("/{id}/edit")
* #Method({"GET", "POST"})
* #Template()
* #param Request $request
* #param Item $item
* #return array|bool|\Symfony\Component\HttpFoundation\RedirectResponse
*/
public function updateENTITYAction(Request $request, Item $item)
{
return parent::updateENTITYAction($request, $item);
}
}
This way you are sending directly the entity to NomenclatorController and you don't even need to know the entityName.
Humm I'll me too advise you to avoid the eval function. It's slow and a bad practice.
What you want here is the factory pattern,
You could define a service to create the entites for you
#app/config/services.yml
app.factory.nomenclators:
class: YourNamespace\To\NomenclatorsFactory
And your factory might be like this
namespace YourNamespace\To;
use YourNamespace\To\Entity as Entites;
class NomenclatorsFactory {
// Populate this array with all your Nomenclators class names with constants OR with reflection if you have many
private $allowedNomemclators = [];
/**
* #param $entityName
* #return NomenclatorsInterface|false
*/
public function getEntity($entityName)
{
if(!is_string($entityName) || !in_array($entityName, $this->allowedNomemclators)) {
// Throw exception or exit false
return false;
}
return new $entityName;
}
}
Then you have to create the NomenclatorsInterface and define in it all the common methods between all your entities. Moreover define one more method getSomeGoodName, the job of this method is to return the good property (area or state)
With this structure your controller can only instances the Nomenclators entities and don't use anymore the eval evil method haha
Moreover you don't have to worry about about the state and area property
Ask if something isn't clear :D
I hope it help !
In my PHP application I have a base class for all database objects, which is further extended by specific model classes. It goes on like this (simplified):
abstract class Model extends Collection {
(...)
function __construct(string $primary_key, string $value) {
$data = MysqlDB::instance() -> select(static::TABLE, implode(',', static::COLUMNS), "$primary_key=\"$value\"");
if (count($data) != 1)
throw new ModelException('Select condition uncertain');
parent::__construct($data[0]);
}
public static function getById(string $id) : ?Model {
try {
return new static('id', $id);
} catch (ModelException $e) {
return NULL;
}
}
(...)
}
The point is, I use the getById function on child classes to obtain the needed objects. However, since the return type of getById method is Model, PhpStorm can't figure out what methods the objects has and highlights the ones I use as an error. Consequently, no type-hinting available.
For instance, assuming that final class User extends Model and that class User has method sendNotification, this kind of code:
User::getById($id) -> sendNotification($param1, $param2, ...)
has sendNotification yellowed out as though it did not exist.
I know this isn't that much of an issue, but it's really irritating in terms of that I get distracted by constant redundant warnings and that I can't use type-hinting in such a usage case.
Try with actual PHPDoc for your getById() where you specify dynamic type (e.g. #return static or #return $this).
That's the most common way of providing such "this class and its' children" typehint.
Another possible option is kind of the same .. but using PHP 7.1 functionality (so no PHPDoc blocks).
Try using self as return type instead of Model. I mean here:
public static function getById(string $id) : ?Model {
use this instead:
public static function getById(string $id) : ?self {
But for this one you should use PhpStorm 2017.2 EAP build as 2017.1.x has issues with that (see https://stackoverflow.com/a/44806801/783119 as an example).
There are three ways to achieve this, none of them has anything to do with PHPStorm.
First overwrite the method in every child with the proper return type:
/**
* #return User
*/
public static function getById(string $id) : ?Model { return parent::getById($id); }
Second add all possible children to the return tag of the abstract class.
/**
* #return Model|User|...
*/
public static function getById(string $id) : ?Model { /* ... */ }
Third add a type hint just in place:
/** #var User $user */
$user = User::getById($id);
$user->sendNotification($param1, $param2, ...);
These are not nice and hard to maintain. But there is no "setting" in PHPStorm for that.
I'm wanting to chain a method with the parent function of itself.
Example:
class Query
{
protected $limit;
/**
* Returns some third object that isn't in this family.
* This object represents the results, and also has
* a first function that gets called in a chain.
*/
public function get()
{
// Do Stuff
return new /* ... */;
}
public function take($amount)
{
$this->limit = $amount;
return $this;
}
}
class ChildQuery extends Query
{
protected $singular = false;
public function get()
{
if($this->singular)
return $this->take(1)->parent::get()->first();
return parent::get()
}
public function singular()
{
$this->singular = true;
return $this;
}
}
This obviously isn't the full set of functions, nor does it work, but you get the idea. I'd like ChildQuery::get to be able to call Query::get in a chain.
Right now, I have to do this:
public function get()
{
$this->take(1);
parent::get()->first();
}
Which is not appealing to me. Any ideas?
I'm running PHP 7, if it matters.
My end result would look something like this:
$query->singular()->get(); // ($query is a ChildQuery)
It is just not possible to call a parent method by the public interface of an object (even if it is the same class/object like the current context). Please have also a look at https://stackoverflow.com/a/11828729/2833639.
In my opinion your solution is the right way to go.
Off Topic: I recommend to read https://ocramius.github.io/blog/fluent-interfaces-are-evil/ to evaluate whether a fluid interface is good for your use case.
I want to write something like (laravel uses):
View::make('FooBarView')->with('foo', $foo)
->with('bar', $bar);
My knowledge and imagination made me to use new self instances. But I don't think that this is the best idea around and I could not handle it.
Google couldn't help me because of my bad keywords I think. I don't want to make you write code for me for sure but what is the name of this design pattern or whatever?
In laravel's source, with function uses
return $this;
But how to use it after make?
By the way, in this example; with method helps you to set variables for view's render.
To call what the function returns, the function will have to return something that is possible to call.
In this case, you could for example return "this":
class View {
/**
* #returns View
*/
public static function make($foo) {
/* do stuff, return new View instance */
return new View();
}
/**
* #returns View
*/
public function with($foo, $bar){
/* do stuff */
return $this;
}
}
That way, whenever you call with you will get the class instance back, which in turn will be callable:
View::make("foo")->with("foo")->with("bar");
// Will be same as:
$v = View::make("foo");
$v = $v->with("foo");
$v = $v->with("bar");
I have Yii application and two tables with same structure tbl and tbl_history:
Now want to create model so it will select table by parameter I send when calling model. For example:
MyModel::model('tbl')->find();
//and
MyModel::model('tbl_history')->find();
Find related article with solution in Yii forum. Made same changes and finally got this in MyModel:
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
Now when I make:
echo MyModel::model('tbl_history')->tableName(); // Output: tbl_history
It returns right value, but:
MyModel::model('tbl_history')->find();
still returns value for tbl.
Added:
public function __construct($id=null,$scenario=null){
var_dump($id);
echo '<br/>';
parent::__construct($scenario);
}
and got:
string(tbl_history)
string(tbl_history)
NULL
It means Yii makes call to model from other place but don't know from where and how to prevent it.
Also It makes 2 calls to model, is it too bad for performance?
It looks like the CActiveRecord::getMetaData() method needs to be overridden to achieve what you are looking for.
<?php
class TestActiveRecord extends CActiveRecord
{
private $tableName = 'tbl'; // <=default value
private static $_models=array();
private $_md;
public function __construct($scenario='insert', $tableName = null)
{
if($this->tableName === 'tbl' && $tableName !== null)
$this->tableName = $tableName;
parent::__construct($scenario);
}
public static function model($tableName = false, $className=__CLASS__)
{
if($tableName === null) $className=null; // this string will save internal CActiveRecord functionality
if(!$tableName)
return parent::model($className);
if(isset(self::$_models[$tableName.$className]))
return self::$_models[$tableName.$className];
else
{
$model=self::$_models[$tableName.$className]=new $className(null);
$model->tableName = $tableName;
$model->_md=new CActiveRecordMetaData($model);
$model->attachBehaviors($model->behaviors());
return $model;
}
}
public function tableName()
{
return $this->tableName;
}
/**
* Returns the meta-data for this AR
* #return CActiveRecordMetaData the meta for this AR class.
*/
public function getMetaData()
{
if($this->_md!==null)
return $this->_md;
else
return $this->_md=static::model($this->tableName())->_md;
}
public function refreshMetaData()
{
$finder=static::model($this->tableName());
$finder->_md=new CActiveRecordMetaData($finder);
if($this!==$finder)
$this->_md=$finder->_md;
}
}
Maybe it's easier to make MyModelHistory which extends MyModel and overrides only one method - tableName().
I recommend implementing single table inheritance. In order to do this you will need to combine your tables with a flag or type column that states whether or not this is a history record. I've pasted a few links at the bottom so you can see how this is implemented in Yii and listed some of the benefits below.
Benefits:
You won't need to duplicate code commonly used between the models
Changes to this table will only need to be executed once.
Changes to the parent model will only need to be made once.
Code becomes generally more maintainable and readable.
You seperate the code that belongs specifically to tbl and tbl_history
http://www.yiiframework.com/wiki/198/single-table-inheritance/
http://en.wikipedia.org/wiki/Single_Table_Inheritance
I created a solution for performing this exact action a couple of months ago. This is a completely dynamic solution, you just pass the table name like you are looking for to the model. That solution was originally designed to work with the same database structure across multiple databases, but it was trivial to adapt it to work in the same database. The documentation for that is here. I'd recommend reading over it as it has more details about CDynamicRecord
It's easy to adapt to work with multiple tables. You can download the adaptation as a gist from github.
Usage
1) Download the Gist and drop it into ext, save as CDynamicRecordSDB.php
2) Create Your model in Model, and setup up as follows:
Basically, you want to extend CDynamicRecord, and override your model() and tableName() so they are compliant with CDyanmicRecord.
<?php
Yii::import('ext.CDynamicRecordSDB');
class Test extends CDynamicRecordSDB
{
public static function model($dbConnectionString = 0, $className=__CLASS__)
{
return parent::model($dbConnectionString, $className);
}
public function tableName()
{
return $this->dbConnectionString;
}
[... Do everything else after this ...]
}
3) Setup your model as you normally would.
Usage
The usage is identical to CActiveRecord, and you can perform all actions. No surprises. Just a couple examples below.
$data = Test::model('tbl')->findAll();
$data2 = new Test('tbl');
$data2->findAll();
foreach ($data as $row)
print_r($row->attributes);
$data = Test::model('tbl_history')->findAll();
foreach ($data as $row)
print_r($row->attributes);
Limitations
The only limitation with doing this is you have to modify how relations work. IF you plan on accessing a related model (Bar), and you have no intention on calling Bar by itself. Then Bar should extend CActiveRecord, and in Foo you can define normal relations. Yii magically carries over the CDbConnectionString across the instances for you.
OTHERWISE, if you intend to access models in the same database, but also want to retain the ability to call them by themselves, then Bar should extend CDynamicModel, and Foo should have a getter defined as follows.
public function getBar()
{
return Bar::model($this->$dbConnectionString);
}
A small way but work for me for any number of table
public static $dynamic_table_name="main_table";
public static function setDynamicTable($param)
{
self::$dynamic_table_name=self::$dynamic_table_name.$param;
}
/**
* #return string the associated database table name
*/
public function tableName($param='')
{
self::setDynamicTable($param);
return self::$dynamic_table_name;
}
// to use it like
ModelName::model()->tableName('_one');
ModelName::model()->tableName('_two');
ModelName::model()->tableName('_three');