How to get grid behavior columns dynamically? - yii2

I'm making a plugin system and I'm dynamically adding behaviors with relation to Order model. For example:
class OrderBehavior extends Behavior
{
public function getOrderTrackNumber()
{
return $this->owner->hasOne(TrackNumber::class, ['order_id' => 'id']);
}
}
At runtime I don't know which plugins (and therefore which behaviors) are activated.
How can I get all relation properties (in example orderTrackNumber) dynamically for render in GridView columns?

You may use getBehaviors() to get all active behaviors attached to model. At this point you should probably implement some interface for behaviors which may add new relations, so they will be able to provide list of defined relations - it may save you performance hell (browsing all behavior's methods and searching relations definitions may be slow and unreliable). For example:
interface BehaviorWithRelationsInterface {
/**
* #return string[]
*/
public function getRelationsNames(): array;
}
Then in model:
/**
* #return string[]
*/
public function getAllRelationsNames(): array {
$relations = [];
foreach ($this->getBehaviors() as $behavior) {
if ($behavior instanceof BehaviorWithRelationsInterface) {
$relations = array_merge($relations, $behavior->getRelationsNames());
}
}
// add relations defined directly in model
$relations[] = 'user';
return $relations;
}
If you don't miss anything, getAllRelationsNames() should return names of all relations defined in model.

there's no built in method in yii2 that returns all relations of a model,
you can either check your attached behaviours via behaviours() method,
or, if you need a complete list, check via Reflection whether your getters and attached behaviors return an object that implements relation interface.

Related

Laravel Converting Existing User System Multi-Type

I recently took over a project that has an existing user system in place (standard Laravel 5.7 make:auth setup). What I've come to identify is that there is a significant amount of MySQL database columns within the user table that are blank or NULL as a result of the varied user types within the system. For the purpose of this example, let's say they are ADMINISTRATOR, PARENTS, CHILDREN. Within the app currently though, there are no clear distinctions as to what a user is other than a column called "type" which is either NULL for children, 'admin' for ADMINISTRATOR, and 'parent' for PARENTS.
I want to be able to seperate all these columns out to be associated with the appropriate user type while maintining the same authentication method (that is my current thought process, not set in stone).
From what I've gathered, I should be looking at a polymorphic relationship in which the User model has a "typeable" morph relationship to each of the user types. I've also read that that entails having a "typeable_type" and "typeable_id" column on the user model where "type" refers to the model - in this case "App\Models\Admin", "App\Models\Parent", "App\Model\Child".
For each of these I would do something like this:
class User extends Model
{
public function typeable()
{
return $this->morphTo();
}
}
class Admin extends Model
{
public function user()
{
return $this->morphMany('App\User', 'typeable');
}
}
class Parent extends Model
{
public function user()
{
return $this->morphMany('App\User', 'typeable');
}
}
class Child extends Model
{
public function user()
{
return $this->morphMany('App\User', 'typeable');
}
}
My question is - how can I create relationships within the "type" classes that can use the parent User ID as the foreign key / basis of any relationship?
For instance, a Parent might have a subscription in the database that perhaps a Admin doesn't. The subscription table references the current user via a "user_id" column.
Traditionally, the User would just reference the subscription through a normal relationship.
class User extends Model
{
public function subscription()
{
return $this->hasOne(App\Models\Subscription::class);
}
}
class User extends Model
{
public function subscription()
{
return $this->hasMany(App\Models\User::class);
}
}
How would I retrieve that in an architecture where that relationship is owned by the "Parent" class. Take in mind, I'm working within a legacy app that I inherited, so everything currently revolves around the user & user_id.
Long story short, I want to be able to pull different types of info depending on user type but still treat each of the user types as a "User" in the traditional sense. An ideal system would be where the information that each user type possesses is "flattened" into the standard user for use.
I don't think using a relationship is what you're really after here.
The way I would set it up is the create a User class (that extends Model) and then have three models extend the User class (Parent, Child and Admin).
In the User base class you would add all code that applies to all users.
I would then add a global scope to each of the Parent, Child and Admin classes (for the example, I've used an anonymous global scope, but you can use a normal scope if you wish):
class Admin extends Users
{
protected static function boot()
{
parent::boot();
static::addGlobalScope('user_type', function (Builder $builder) {
$builder->where('type', '=', 'admin');
});
}
}
You would then add any relevant methods into each of the three classes, for example, your subscription relationship would look like this:
class Admin extends User
{
public function subscription()
{
return $this->hasOne(Subscription::class);
}
}
class Subscription extends Model
{
public function admins()
{
return $this->hasMany(Admin::class);
}
}
You then might also need to set the table in each of the three classes by using protected $table = 'users' - but I'm not certain on this.
If you were then provided just a user class and wanted to convert that user into one of the specific base classes, you could add a function like this to the class:
class User extends Model
{
public function getUserType() // Or whatever you would like the name to be
{
// NOTE: I've made this function load without having to do any database calls
// but if you don't mind an additional call, it might be easier to just change
// the switch to return Admin::find($this->id);
$class = null;
switch($this->type) {
case "admin":
$class = new Admin($this->attributesToArray());
break;
case "parent":
$class = new Parent($this->attributesToArray());
break;
case "child":
$class = new Child($this->attributesToArray());
break;
}
if(is_null($class)) {
// Throw error, return null, whatever you like
return $class;
}
// Perform any additional initialisation here, like loading relationships
// if required or adding anything that isn't an attribute
return $class;
}
}

What is the best practice within yii2 to create a factory class?

So basically, I want to create implement a factory class that will use a couple different models. I don't have a clue as to how I should go about doing this within yii2. Any help would be nice.
Here is a general idea of what I am trying to do.
use app\models\Event;
use app\models\EventParticipant;
use app\models\Match;
/**
* #property Event $Event
* #property EventParticipant $EventParticipant
* #property Match $Match
*/
abstract class Tournament
{
protected $_id;
protected $_event;
protected $_type;
final public function __construct($event) {
$this->Event = new Event();
$this->EventParticipant = new EventParticipant();
$this->Match = new Match();
if(!$event) {
throw new \yii\web\HttpException(400, 'Invalid Event', 405);
}
$this->_id = $event['Event']['id'];
}
}
}
I would avoid throwing Http exceptions in models, use them in Controllers. You can throw InvalidConfigurationException for example, not necessary as you need to have $event.
There are many implementations of Factory design pattern, here is the simplest
class TournamentFactory
{
public static function create(Event $event, EventParticipant $eventParticipant, Match $match) {
return new Tournament($event, $eventParticipant, $match);
}
}
but I don't see it's use in this example. I mostly use it to switch between object types, something like this, in your example:
$grandSlam = TournamentFacory::create('grandSlam');
$grandSlam->setEvent($event);
$grandSlam->setParticipants($participants);
...
$masters = TournamentFacory::create('masters');
...
these objects might have same properties in common, but different implementations. For example masters are played to two winning sets, grand slams on 3...etc...

Yii2 field accessed only via magic method

/**
* This is the model class for table "hashtag".
*
* #property string $text
*
* #property TweetHashtag[] $tweetHashtags
* #property Tweet[] $tweets
*/
class Hashtag extends ActiveRecord
{
.........
public function getTweetHashtags()
{
return $this->hasMany(TweetHashtag::className(), ['hashtag_text' => 'text']);
}
/**
* #return \yii\db\ActiveQuery
*/
public function getTweets()
{
return $this->hasMany(Tweet::className(), ['id' => 'tweet_id'])->viaTable('tweet_hashtag', ['hashtag_text' => 'text']);
}
}
When I do in some component
$hashtags = Hashtag::find()
->with('tweets')
->where(['text' => $hashtagText])
->all();
foreach($hashtags as $hashtag)
{
print_r($hashtag->tweets);
}
It`s working but why tweets - field accessed only via magic method and how can i fix it? And tweetHashtags working well.
Class Tweet have same relationship but public function getHashtags() working without this problem.
Your question is not clear. Each method on a Component class that start with get (like getName) can be accessed with property form (e.g. name). On special case, relations of Yii's ActiveRecord, if you access to relation by property form, you get results. In fact $this->tweets is a shorthand for $this->getTweets()->all().
P.S: On Yii2 Document http://www.yiiframework.com/doc-2.0/guide-db-active-record.html#accessing-relational-data:
Note: While this concept looks similar to the object property feature,
there is an important difference. For normal object properties the
property value is of the same type as the defining getter method. A
relation method however returns an yii\db\ActiveQuery instance, while
accessing a relation property will either return a yii\db\ActiveRecord
instance or an array of these.
$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance
This is useful for creating customized queries, which is described in the next section.

Yii2: Can I use scenarios to specify different set of model fields for different actions?

I can see in the Yii2 model page (http://www.yiiframework.com/doc-2.0/yii-base-model.html), in the "fields" section that you can set "different lists of fields based on some context information. For example, depending on $scenario or the privilege of the current application user, you may return different sets of visible fields or filter out some fields."
But, scenarios documentation (http://www.yiiframework.com/doc-2.0/guide-structure-models.html#scenarios) says scenarios is for creating different context for model attributes validation.
I'm using Yii2 Restful API, where I have to use default actions (actionIndex, actionView, ...) to get data from model and show as API results. I know I can override those methods (http://www.yiiframework.com/doc-2.0/guide-rest-controllers.html#extending-active-controller), but how can I say in those methods to use different set of fields (depending on different scenarios) ?
What I need is to output field1, field2, field3 for actionIndex (items list), but I want to output field1, field2, field3, field4 for actionView (item list).
You do it in your model.
Default REST implementation in Yii2 will only include attributes that are returned by the fields() method. By default, that method returns all attributes. Therefore, you define it like so:
class MyModel extends ActiveRecord
{
//...
public function fields()
{
switch ($this->scenario) {
case 'my_scenario_1':
return ['field1', 'field2'];
case 'my_scenario_2':
return ['field3', 'field4'];
}
}
}
Also, you have the scenarios() method at your disposal, which returns all active attributes for a given scenario.
Don't forget to set the models' scenario in your controller.
According to this guide: http://www.yiiframework.com/doc-2.0/guide-structure-models.html#scenarios
first set your scenarios in your model. example:
const SCENARIO_LESS = 'index';
const SCENARIO_MORE = 'view';
public function scenarios()
{
return [
self::SCENARIO_LESS => ['field1', 'field2'],
self::SCENARIO_MORE => ['field1', 'field2', 'field3'],
];
}
now in your action(s) just set the scenario like this:
// scenario is set through configuration
public function actionIndex()
{
$model = new User(['scenario' => User::SCENARIO_LESS]);
...
}
...
public function actionView()
{
$model = new User(['scenario' => User::SCENARIO_MORE]);
...
}
that should do it.
this is my solution for this problems, using Yii::$app()
public function fields()
{
if(Yii::$app->controller->action->uniqueId == 'controller/action'){
return ['field_1','field_2','field_3','field_4'];
}else{
return ['field_1','field_3'];
}
}
hope works for you

How can I create a subclass that takes in different parameters for the same function name?

So I have made this simple interface:
package{
public interface GraphADT{
function addNode(newNode:Node):Boolean;
}
}
I have also created a simple class Graph:
package{
public class Graph implements GraphADT{
protected var nodes:LinkedList;
public function Graph(){
nodes = new LinkedList();
}
public function addNode (newNode:Node):Boolean{
return nodes.add(newNode);
}
}
last but not least I have created another simple class AdjacancyListGraph:
package{
public class AdjacancyListGraph extends Graph{
public function AdjacancyListGraph(){
super();
}
override public function addNode(newNode:AwareNode):Boolean{
return nodes.add(newNode);
}
}
Having this setup here is giving me errors, namely:
1144: Interface method addNode in namespace GraphADT is implemented with an incompatible signature in class AdjacancyListGraph.
Upon closer inspection it was apparent that AS3 doesn't like the different parameter types from the different Graph classes newNode:Node from Graph , and newNode:AwareNode from AdjacancyListGraph
However I don't understand why that would be a problem since AwareNode is a subClass of Node.
Is there any way I can make my code work, while keeping the integrity of the code?
Simple answer:
If you don't really, really need your 'addNode()' function to accept only an AwareNode, you can just change the parameter type to Node. Since AwareNode extends Node, you can pass in an AwareNode without problems. You could check for type correctness within the function body :
subclass... {
override public function addNode (node:Node ) : Boolean {
if (node is AwareNode) return nodes.add(node);
return false;
}
}
Longer answer:
I agree with #32bitkid that your are getting an error, because the parameter type defined for addNode() in your interface differs from the type in your subclass.
However, the main problem at hand is that ActionScript generally does not allow function overloading (having more than one method of the same name, but with different parameters or return values), because each function is treated like a generic class member - the same way a variable is. You might call a function like this:
myClass.addNode (node);
but you might also call it like this:
myClass["addNode"](node);
Each member is stored by name - and you can always use that name to access it. Unfortunately, this means that you are only allowed to use each function name once within a class, regardless of how many parameters of which type it takes - nothing comes without a price: You gain flexibility in one regard, you lose some comfort in another.
Hence, you are only allowed to override methods with the exact same signature - it's a way to make you stick to what you decided upon when you wrote the base class. While you could obviously argue that this is a bad idea, and that it makes more sense to use overloading or allow different signatures in subclasses, there are some advantages to the way that AS handles functions, which will eventually help you solve your problem: You can use a type-checking function, or even pass one on as a parameter!
Consider this:
class... {
protected function check (node:Node) : Boolean {
return node is Node;
}
public function addNode (node:Node) : Boolean {
if (check(node)) return nodes.add(node);
return false;
}
}
In this example, you could override check (node:Node):
subclass... {
override protected function check (node:Node) : Boolean {
return node is AwareNode;
}
}
and achieve the exact same effect you desired, without breaking the interface contract - except, in your example, the compiler would throw an error if you passed in the wrong type, while in this one, the mistake would only be visible at runtime (a false return value).
You can also make this even more dynamic:
class... {
public function addNode (node:Node, check : Function ) : Boolean {
if (check(node)) return nodes.add(node);
return false;
}
}
Note that this addNode function accepts a Function as a parameter, and that we call that function instead of a class method:
var f:Function = function (node:Node) : Boolean {
return node is AwareNode;
}
addNode (node, f);
This would allow you to become very flexible with your implementation - you can even do plausibility checks in the anonymous function, such as verifying the node's content. And you wouldn't even have to extend your class, unless you were going to add other functionality than just type correctness.
Having an interface will also allow you to create implementations that don't inherit from the original base class - you can write a whole different class hierarchy, it only has to implement the interface, and all your previous code will remain valid.
I guess the question is really this: What are you trying to accomplish?
As to why you are getting an error, consider this:
public class AnotherNode extends Node { }
and then:
var alGraph:AdjacancyListGraph = new AdjacancyListGraph();
alGraph.addNode(new AnotherNode());
// Wont work. AnotherNode isn't compatable with the signature
// for addNode(node:AwareNode)
// but what about the contract?
var igraphADT:GraphADT = GraphADT(alGraph);
igraphADT.addNode(new AnotherNode()); // WTF?
According to the interface this should be fine. But your implemenation says otherwise, your implemenation says that it will only accept a AwareNode. There is an obvious mismatch. If you are going to have an interface, a contract that your object should follow, then you might as well follow it. Otherwise, whats the point of the interface in the first place.
I submit that architecture messed up somewhere if you are trying to do this. Even if the language were to support it, I would say that its a "Bad Idea™"
There's an easier way, then suggested above, but less safe:
public class Parent {
public function get foo():Function { return this._foo; }
protected var _foo:Function = function(node:Node):void { ... }}
public class Child extends Parent {
public function Child() {
super();
this._foo = function(node:AnotherNode):void { ... }}}
Of course _foo needs not be declared in place, the syntax used is for shortness and demonstration purposes only.
You will loose the ability of the compiler to check types, but the runtime type matching will still apply.
Yet another way to go about it - don't declare methods in the classes they specialize on, rather make them static, then you will not inherit them automatically:
public class Parent {
public static function foo(parent:Parent, node:Node):Function { ... }}
public class Child extends Parent {
public static function foo(parent:Child, node:Node):Function { ... }}
Note that in second case protected fields are accessible inside the static method, so you can achieve certain encapsulation. Besides, if you have a lot of Parent or Child instances, you will save on individual instance memory footprint (as static methods therefore static there exists only one copy of them, but instance methods would be copied for each instance). The disadvantage is that you won't be able to use interfaces (can be actually an improvement... depends on your personal preferences).