Yii2: Configurable models inside module - activerecord

What is the best practice of including models/activerecords within a Yii2 module in a way that they are configurable?
These are just some of the problems we face when we want to use an activerecord included inside a module:
Adding events & behaviors to models/activerecords provided by a module. I want to attach events and behaviors to the models included in a module using Yii2's configuration format. How can this be done?
Defining relations with models/activerecords that exist outside of the module. When linking an activerecord contained inside a module to the User activerecord we can rely on Ỳii::$app->user->identityClass, but for other custom relations we might need to extend the activerecord. Is there any better approach? Extending activerecord classes from modules somewhat defeats the purpose of modularity.
Configuring various other variables within the module/activerecord. Let's say we want to adjust the max string length validation value. In a module Controller, we can always use $this->module->params to read any custom value, but we cannot do this from a Model or an ActiveRecord. What are we supposed to do instead?

I think you might end up using dependency injection:
Write an extension "\common\extensions\MyBootstrap":
namespace common\extensions;
use Yii;
use yii\base\BootstrapInterface;
use yii\base\Application;
class MyBootstrap implements BootstrapInterface {
/**
* #param Application $app Application
**/
public function bootstrap($app) {
Yii::$container->set("common\\modules\\test\\models\\Test1", "common\\modules\\test\\models\\Test2");
}
}
add to your config:
'bootstrap' => [
'common\extensions\MyBootstrap',
],
'components' => [
// ...
]
and in your code you have to use Yii::$container->get():
$test = Yii::$container->get('common\modules\test\models\Test1');
var_dump($test);
which will create Test2 model instead of Test1.
If you want this to happen for your ActiveRecord, override this:
public static function instantiate($row) {
return \Yii::$container->get(static::class);
}

EDIT: The underlying issue has now been resolved. We can use DI to inject relations into ActiveRecords.
As of July 2017, Yii2 does not allow ActiveRecord dependency injection!
See:
https://github.com/yiisoft/yii2/issues/8639
https://github.com/yiisoft/yii2/issues/11575
https://github.com/yiisoft/yii2/issues/5786
https://github.com/yiisoft/yii2/pull/14078
https://github.com/yiisoft/yii2/issues/13779
The only way around this is to configure your modules through your Yii::$app->params and then use those values inside module ARs (eg. when doing validation).

Related

Laravel: to make external modules independent

We developed a modular project in Laravel 5.1. There are lots of core modules and models that use these modules. In our case, if the inserted module uses other modules, it will be related to those models dynamically.
When I remove the module from project by hand, I need to remove its dependencies from each module. We want establish relations without creating dependency between modules.
For example;
User model from account module used by other many other modules. Assume we have discussion module.
When we build a relationship for discussion model, we can reach the user of the corresponding model. However, if we establish a relationship with the user model, this project will no longer be a moduler.
We want to add dynamic functions to the user module from discussion module.
Temporarily, we add this code fragment to user module.
/**
* #return mixed
*/
public function lastAnswer() {
if( class_exists( Config::get( 'account.models.discussion' ) ) ) {
return $this->hasOne( Config::get( 'account.models.answer' ) )->latest();
}
return null;
}
If the config file has a relation, we link it, otherwise it will not be linked or we will return null.
But we want to add this dinamically from discussion module rather than account module.
If we accomplish this, whenever we add or remove the discussion module from project, it will continue to run without problem.
We tried to add laravel macroable as a trait but we could't make it work in model files. It gives scope error.
For transformers files we are able to do this but in model files it didn't work.

Composer package - use custom model if exists

My apologies if this exists already but my search-fu can not find the answer.
I have a composer package, and want to use my model ONLY IF an existing model doesn't exist (or extend the custom model), but I can't seem to figure out how to specify the "use" command properly inside my composer model. Since I won't know the name of the "app" using the package, I can't extend it.
<?php
namespace MyComposer\Package\Models;
use Illuminate\Database\Eloquent\Model;
class MyPackageModel extends Model
{
If I put it as a config option, I can't use that in the extends i.e class MyPackageModel extends config('custom_model_name')
I had thought I should do the check in the ServiceProvider but I can't seem to find the right code to register the proper model name to use in there.
Thanks.
I've done something similar to this before, I believe. But my approach was slightly different. See if it makes sense:
Create a base class in your own package. This will be the fallback
model which will be used if the "local" package (the one consuming
your package) doesn't have it's own version of it;
Create a config file which states which model will be used. The default is the model inside your own package (i.e. the fallback);
After installing and setting up your package, if a user does nothing they will automatically have your base model available. If they wish to override your base model with a custom local version, they can simply extend your base model and alter the model to be used in their config file.
I've also found that sometimes it's useful for the base model to 1) implement an interface that can be checked in your package's logic without relying on a specific class (which, after all, is meant to be overridden, right?); and 2) have most of it's logic inside a trait which the "local" model can use without ever having to extend your model (crucial if the local model already extends some other class for whatever reason).
How you approach the code would very much depend what you plan to do with that model. Say, for example, you have a supporting class that creates media entries in your database. Here's your packages model:
<?php
namespace Namespace\Package;
class Media
{
//...
}
And here's the default config:
<?php
return [
'model' => \Namespace\Package\Media::class,
];
And here's a sample manipulation, where you actually account for the local app to override your own model:
<?php
namespace Namespace\Package;
class MediaManager
{
protected function getModel()
{
$model = config('package.model');
return new $model;
}
public function createMedia($attributes = [])
{
$media = $this->getModel($attributes);
$media->save();
return $media;
}
}
That is to say, you never reference any Media model literally. You do your manipulations via the MediaManager. Of course the logic is very simplistic, but hopefully it's enough to get the bigger picture.

Laravel constants in class Facades

I have a class called Awesome and have used the ServiceProvider and the Facade to register it to the app. Now I can use it as Awesome::Things().
I want to add constants to this class, so I tried
<?php namespace Helper\Awesome;
class Awesome()
{
public static $MOVIE = 'I love the Lego Movie!";
}
but when I call Awesome::$MOVIE, I get Access to undeclared static property: Helper\\Aesome\\Facades\\AwesomeFacade::$MOVIE
Can someone help?
The short version is -- you don't really want to do that. Laravel facades aren't mean to be used like normal classes, and if your application uses them that way you'll likely confuse future developers.
Warning out of the way. When you create a "facade" in Laravel, you're actually creating a class alias. When you added Awesome to the alias list in app/config/app.php, at some point code like the following ran
class_alias('Helper\Aesome\Facades\AwesomeFacade','Awesome');
That means whenever you use a global non-namespaced class Awesome, PHP substitutes Helper\Aesome\Facades\AwesomeFacade. If you wanted to add constants, you'd need to add them to this class.
Laravel's able to pass through methods because of the base Facade class implements a __callStatic method that passes on your call to the actual service implementation object. Facades don't pass on static constant access. Additionally, PHP does not (appear to?) have similar magic methods for passing along requests for constants.
If you're curious about the in depth version of this answer, I'm currently writing a series on Laravel's object system, including some in-depth information about the facade implementation.

How to autoload multiple classes in Laravel 4 application?

I’ve created a workbench package in Laravel 4, which is name-spaced and has two directories: Models and Contexts. Somehow, Laravel is loading my models in my Models directory (I have a model in there called User), however, it doesn’t know about my classes in the Contexts directory.
I want to be able to use my context classes in my app’s controllers without specifying the whole namespace, so I thought I’d add them to Laravel’s IoC container. However, it seems I need to create a façade class for each class I wish to add to the container. This isn’t desirable if I have dozens of context classes, as it would mean creating an equal amount of façade classes too.
Is there a way in Laravel to bulk-add classes to its IoC container?
if you want to use one term facades for your classes the laravel way (e.g. MyModel::someAction()) then you have to create your facades. but i'd advise not to do so for so many classes.
if your classes inside contexts folder aren't found then you should check your composer.json file under the autoload entry or do a composer dump-autoload -o.
I'd just DI the classes within the constructor of the class that uses them, so you end up using $this->myService->someAction().
This should answer both Laravel 4 and 5.
First, you need to use the bind method Illuminate\Foundation\Application class, which serves to register binding in the service container. In the Laravel documentation you will find plenty of examples how to do that, but only for a single binding.
If you take a look a the implementation of bind method here or just the definition here, you will notice that this method accepts a string|array. This means you can provide multiple bindings as an array and register all of them in the container with their fully qualified class names. Something like this:
$this->app->bind(['\App\Acme\Service1', '\App\Acme\Service2', '\App\Acme\Service3', ...]
Having this in mind, you can easily get the classes in one namespace (directory) with a reflection, put them in array and use the above method to register them.
Revisiting this question some time later, I think the appropriate solution would be to autoload the classes using my package’s composer.json file, and then import classes using it’s FQN in controllers and other classes:
<?php
use Vendor\Package\Contexts\ContextClass;
class Laravel4Controller extends BaseController {
protected $context;
public function __construct(ContextClass $context) {
$this->context = $context;
}
}

CodeIgniter - where to put functions / classes?

Am having problems understanding where classes should be kept in CI. I am building an application that describes / markets mobile phones.
I would like for all of my functions (i.e. getphone, getdetails etc.) to reside in one class called Mobile - I understand that this file should be called Mobile.php and reside in the controllers folder.
Can I then have multiple functions inside Mobile.php? E.g.
public function getphone() {
xxx
xx
xx
}
public function getdetails() {
xxx
xx
xx
}
Or do I need to put each function in its own class?
I'd really appreciate looking at some sample code that works. I've been going through the documentation and google for a few hours, and tried all sorts of variations in the URL to find a test class, but without much luck! I've even messed around with the routes and .htaccess...
All I am trying to achieve is the following:
http:///model/HTC-Desire/ to be re-routed to a function that accepts HTC-Desire as a parameter (as I need it for a DB lookup). The default controller works fine, but can't get anything to work thereafter.
Any ideas?
Thanks
Actually it works like this:
Controllers and Models go to their perspective folders as you know it
If you want to create functions that are not methods of an object, you must create a helper file. More info here :
http://codeigniter.com/user_guide/general/helpers.html
Now if you want to create your own datatypes (classes that don't extend Models and Controllers), you add them to the library folder. So if let's say you want to create a class "Car" you create this file:
class Car{
function __construct(){}
}
and save it in the libraries folder as car.php
To create an instance of the Car class you must do the following:
$this->load->library('car');
$my_car = new Car();
More information on libraries here:
http://codeigniter.com/user_guide/general/creating_libraries.html
Yes, you can have as many functions in a controller class as you'd like. They are accessible via the url /class/function.
You can catch parameters in the class functions, though it's not advisable.
class Mobile extends CI_Controller{
public function getPhone($phoneModel=''){
echo $phoneModel;
//echo $this->input->post('phoneModel');
}
}
http://site.com/mobile/getPhone/HTC-Rad theoretically would echo out "HTC-Rad". HOWEVER, special characters are not welcome in URL's in CI by default, so in this example you may be met with a 'Disallowed URI characters" error instead. You'd be better off passing the phone model (or any other parameters) via $_POST to the controller.
Classes can exist both as Controllers and Models, as CodeIgniter implements the MVC pattern. I recommend reading more about that to understand how your classes/functions/etc. can best be organized.
Off the top of my head, Pyro CMS is an application built with CodeIgniter and the source code is freely available. I'm sure there are others.
I think it's best you handle it from one perspective, that is; create a utility class with all your functions in it.
The answer to the question of where to put/place the class file is the "libraries" folder.
This is clearly stated in the documentation. Place your class in the libraries folder.
When we use the term “Libraries” we are normally referring to the
classes that are located in the libraries directory and described in
the Class Reference of this user guide.
You can read more on creating and using libraries Creating Libraries — CodeIgniter 3.1.10 documentation
After placing the newly created class in the libraries folder, to use just simply load the library within your controller as shown below:
$this->load->library('yourphpclassname');
If you wish to receive several arguments within you constructor you have to modify it to receive an argument which would be an array and you loading/initialization would then be slightly different as shown below:
$params = array('type' => 'large', 'color' => 'red');
$this->load->library('yourphpclassname', $params);
Then, to access any of the functions within the class simply do that as shown below:
$this->yourphpclassname->some_method();
I hope this answers your question if you have further question do leave a comment and I would do well to respond to them.

Resources