yii2 correct application architecture (pass data to layout) - php

i need to send data from controllers to main layout (something like notifier of a new messages in top menu) i need it in all app(global)
i found one way to to pass variables to layout
Yii::$app->controller->myvar
from class property to layout, but i think it's not best way to duplicate code to all controllers, maybe i suppose to extend base Controller and set my logic here?
Tell me please the best practice to do what i want.
ps. Tnx and sorry for my English

In the controller, you can use
$this->view->params['name'] = 123
and in the layout
<?= $this->params['name'] ?>

1) You can use yii global app params
Yii::$app->params['foo'] = 'bar'; // controller
and
echo Yii::$app->params['foo']; // in view/layout/controllers/elsewhere
2) You can use session. Create a controller, that will be extended by others, with these 3 functions:
<?php
namespace common\components;
use Yii;
use yii\web\Controller;
class BaseController extends Controller
{
/**
* Set user flash notice
* #param $message
* #return mixed
*/
public function setFlash($key, $message){
return Yii::$app->session->setFlash($key, $message);
}
/**
* Has user flash notice
* #param $message
* #return mixed
*/
public function hasFlash($key){
if(Yii::$app->session->hasFlash($key)) return True;
else return false;
}
/**
* Get user flash notice
* #param $message
* #return mixed
*/
public function getFlash($key){
return Yii::$app->session->getFlash($key);
}
}
now in your controllers
use common\components\BaseController;
...
class MyController extends BaseController
...
$this->setFlash('foo','bar'); // setting flash var
and in your views
echo $this->context->getFlash('foo'); // getting flash var
or
echo Yii::$app->controller->getFlash('foo'); // getting flash var

The below line add in config\main.php
'user'=>array( 'class'=>'WebUser', // enable cookie-based authentication 'allowAutoLogin'=>true, ),
After that create new file in protected\components\WebUser.php, In this WebUser.php file
class WebUser extends CWebUser
{
private $_model;
function Update()
{
return $this->myvar='this is my variable';
}
}
You can access in layout file like this echo Yii::app()->user->update();

Related

Laravel dependancy injection for views

I'm trying to get the hang of dependancy injection and the IoC container in Laravel.
Currently I have a class full of static methods that I use in my views. E.g.
class Templatizer {
/**
* Generates a colored FontAwsome check or cross from a true/false argument
* #param boolean $bool
* #return string The HTML markup of the icon
*/
public static function boolicon($bool)
{
return $bool ? '<span class="fa fa-check text-success"></span>' : '<span class="fa fa-times text-danger"></span>';
}
}
I have composer autoload the class and in my view I can just go {{ Templatizer::boolicon($trueOrFalseValue) }}.
Clearly this is bad practice and I'd like to move away from using static methods. I presume the correct way is to inject an instance of Templatizer and use the methods something like {{ $templatizer->boolicon($v) }}`.
How would I structure this? Presumably I'd need to inject an instance of Templatizer into my controller via the constructor? e.g.
class PagesController extends BaseController {
protected $templatizer;
public function __construct(Templatizer $templatizer)
{
$this->templatizer = $templatizer;
}
}
And then for, say, the method for the index page I pass the instance to the view? e.g.
# inside PagesController
public function index()
{
return View::make('pages.index', ['templatizer' => $this->templatizer]);
}
If this is correct, where is an appropriate place to put my Templatizer class? How would I bind it to the IoC container?
First of all I don't see anything wrong with calling these methods statically. It looks like this is just a HTML helper class of your own and you should be fine.
If you decide to go with Dependency Injection the "correct" way to register your class would be using Service Providers.
Assigning it to the views the way you wrote would work but you can also get it once you need it this way:
$templatizer = App::make('Yournamespace\Templatizer');
And finally probably the best solution in your case would be building your own Facade.
Yes you should inject it via the controller constructor, the files can live anywhere you like as long as they are autoloaded in.
I like to create a folder in the root directory called src so that my composer.json file looks like:
"autoload": {
"classmap": [
....
],
"psr-4": {
"Foo\\": "src/"
}
}
Then you can have src/Templatizer.php which would look like:
<?php namespace Foo;
class Templatizer {
}
Now you just need a service provider to bind your instance of Templatizer (this basically makes Laravel aware of your class and allows you to inject it into your controllers) in src/FooServiceProvider.php
<?php namespace Foo;
use Illuminate\Support\ServiceProvider;
class FooServiceProvider extends ServiceProvider {
/**
* Indicates if loading of the provider is deferred
*
* #var boolean
*/
protected $defer = false;
/**
* Register the service provider
*/
public function register() {
$this->app->bind('Foo\Templatizer', function($app) {
return new Templatizer();
});
}
}
Don't forget to add Foo\FooServiceProvider to the providers array in app config and you should be all set to do...
public function __construct(Foo\Templatizer $templatizer) {
You are also create instance on BaseController.
class BaseController extends Controller {
/**
* Setup the layout used by the controller.
*
* #return void
*/
var $templatizer;
protected function setupLayout()
{
if ( ! is_null($this->layout))
{
$this->layout = View::make($this->layout);
$templatizer = new \Templatizer();
View::share('templatizer', $templatizer);
}
}
}
use this $templatizer instance in a all view.
something like {{ $templatizer->boolicon($v) }}`.

PHP/Laravel magic, how does this work?

I'm looking at the Laravel docs, and I see this snippet:
class UserController extends BaseController {
/**
* The layout that should be used for responses.
*/
protected $layout = 'layouts.master';
/**
* Show the user profile.
*/
public function showProfile()
{
$this->layout->content = View::make('user.profile');
}
}
we can clearly see that $this->layout = 'layouts.master'. However, then they define a child of the layout object (which as I understand is only a base PHP string, and does not have a field called content, via ...
$this->layout->content = View::make('user.profile');
How can a string have a field called content defined?
when I subclass BaseController and try to assign a value to
$this->layout->content, why do I get the following error: "Attempt
to assign property of non-object"?
Why not look at BaseController? It looks like they change $this->layout.
https://github.com/laravel/laravel/blob/master/app/controllers/BaseController.php
<?php
class BaseController extends Controller {
/**
* Setup the layout used by the controller.
*
* #return void
*/
protected function setupLayout()
{
if ( ! is_null($this->layout))
{
$this->layout = View::make($this->layout);
}
}
}
My advice, if you use a PHP framework, don't be afraid to check its source when you don't understand why it works. There is usually much less "magic" than you think...
IMO, this seems like a bad design though, to initialize the variable as a string and then change it to be some object. Kind of just abusing the loose typing.

Laravel can't instantiate interface via __construct (using App::bind)

I am trying to resolve class via __construct using Laravel's bind() method.
Here what I do:
routes.php (of course I will move it away from here)
// Bindings
App::bind(
'License\Services\ModuleSelector\SelectorInterface',
'License\Services\ModuleSelector\ModuleSelector'
);
SelectorInterface.php - interface that I will expect in __construct method.
<?php namespace License\Services\ModuleSelector;
interface SelectorInterface {
/**
* Simply return query that will select needle module fields
*
* #return mixed
*/
public function make();
}
ModuleSelector.php - this is class that I want to resolve via Laravel's DI (see example below).
<?php namespace License\Services\ModuleSelector;
use License\Services\ModuleSelector\Selector;
class ModuleSelector extends Selector
{
/**
* Get module by it's code
*
* #return mixed
*/
public function find()
{
return $this->make()
->where('code', $module_code)
->first();
}
}
Module.php
<?php namespace License\Services\ModuleType;
use License\Services\ModuleType\TypeInterface;
use License\Services\ModuleSelector\SelectorInterface;
class Module
{
...
function __construct(SelectorInterface $selector)
{
$this->selector = $selector;
}
...
}
And the place when error occurs:
In my repo I have use License\Services\ModuleType\Module as ModuleService;.
Than there is method called find():
/**
* Find module by its code with all data (types, selected type)
* #return mixed
*/
public function find($module_code)
{
$module = new ModuleService;
// Get module id in order to use build in relations in framework
$module = $this->module->find($module_code);
...
}
So, in other words, I have 2 classes and one interface. What I am trying to do is:
1) Create Class1.php / Class2.php / Class2Interface.php.
2) In Class1.php in the __construct I specify __construct(Class2Interface $class2).
3) Instantiate Class2.
What I am doing wrong? Examples found here.
In this line:
$module = new ModuleService;
You are directly invoking the Module class and not passing in an instance of SelectorInterface.
For the IoC to work you bind and make classes using it. Try that line again with :
$module = App::make('License\Services\ModuleSelector\SelectorInterface');
An alernative is to inject it directly into your repos constructor, as long as the repo is created by the IoC container, your concrete will be automatically injected.
Nowhere do you have a class marked to actually "implement SelectorInterface".

PHPSpec and Laravel - how to handle double method not found issues

I appear to be having issues with my spec tests when it comes to stubs that are calling other methods.
I've been following Laracasts 'hexagonal' approach for my controller to ensure it is only responsible for the HTTP layer.
Controller
<?php
use Apes\Utilities\Connect;
use \OAuth;
class FacebookConnectController extends \BaseController {
/**
* #var $connect
*/
protected $connect;
/**
* Instantiates $connect
*
* #param $connect
*/
function __construct()
{
$this->connect = new Connect($this, OAuth::consumer('Facebook'));
}
/**
* Login user with facebook
*
* #return void
*/
public function initialise() {
// TODO: Actually probably not needed as we'll control
// whether this controller is called via a filter or similar
if(Auth::user()) return Redirect::to('/');
return $this->connect->loginOrCreate(Input::all());
}
/**
* User authenticated, return to main game view
* #return Response
*/
public function facebookConnectSucceeds()
{
return Redirect::to('/');
}
}
So when the route is initialised I construct a new Connect instance and I pass an instance of $this class to my Connect class (to act as a listener) and call the loginOrCreate method.
Apes\Utilities\Connect
<?php
namespace Apes\Utilities;
use Apes\Creators\Account;
use Illuminate\Database\Eloquent\Model;
use \User;
use \Auth;
use \Carbon\Carbon as Carbon;
class Connect
{
/**
* #var $facebookConnect
*/
protected $facebookConnect;
/**
* #var $account
*/
protected $account;
/**
* #var $facebookAuthorizationUri
*/
// protected $facebookAuthorizationUri;
/**
* #var $listener
*/
protected $listener;
public function __construct($listener, $facebookConnect)
{
$this->listener = $listener;
$this->facebookConnect = $facebookConnect;
$this->account = new Account();
}
public function loginOrCreate($input)
{
// Not the focus of this test
if(!isset($input['code'])){
return $this->handleOtherRequests($input);
}
// Trying to stub this method is my main issue
$facebookUserData = $this->getFacebookUserData($input['code']);
$user = User::where('email', '=', $facebookUserData->email)->first();
if(!$user){
// Not the focus of this test
$user = $this->createAccount($facebookUserData);
}
Auth::login($user, true);
// I want to test that this method is called
return $this->listener->facebookConnectSucceeds();
}
public function getFacebookUserData($code)
{
// I can't seem to stub this method because it's making another method call
$token = $this->facebookConnect->requestAccessToken($code);
return (object) json_decode($this->facebookConnect->request( '/me' ), true);
}
// Various other methods not relevant to this question
I've tried to trim this down to focus on the methods under test and my understanding thus far as to what is going wrong.
Connect Spec
<?php
namespace spec\Apes\Utilities;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use \Illuminate\Routing\Controllers\Controller;
use \OAuth;
use \Apes\Creators\Account;
class ConnectSpec extends ObjectBehavior
{
function let(\FacebookConnectController $listener, \OAuth $facebookConnect, \Apes\Creators\Account $account)
{
$this->beConstructedWith($listener, $facebookConnect, $account);
}
function it_should_login_the_user($listener)
{
$input = ['code' => 'afacebooktoken'];
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
So here's the spec that I'm having issues with. First I pretend that I've got a facebook token already. Then, where things are failing, is that I need to fudge that the getFacebookUserData method will return a sample user that exists in my users table.
However when I run the test I get:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` not found.
I had hoped that 'willReturn' would just ignore whatever was happening in the getFacebookUserData method as I'm testing that separately, but it seems not.
Any recommendations on what I should be doing?
Do I need to pull all of the OAuth class methods into their own class or something? It seems strange to me that I might need to do that considering OAuth is already its own class. Is there some way to stub the method in getFacebookUserData?
Update 1
So I tried stubbing the method that's being called inside getFacebookUserData and my updated spec looks like this:
function it_should_login_the_user($listener, $facebookConnect)
{
$returnCurrentUser = (object) [
'email' => 'existinguser#domain.tld',
];
$input = ['code' => 'afacebooktoken'];
// Try stubbing any methods that are called in getFacebookUserData
$facebookConnect->requestAccessToken($input)->willReturn('alongstring');
$facebookConnect->request($input)->willReturn($returnCurrentUser);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
$listener->facebookConnectSucceeds()->shouldBeCalled();
$this->loginOrCreate($input);
}
The spec still fails but the error has changed:
Apes/Utilities/Connect
37 ! it should login the user
method `Double\Artdarek\OAuth\Facade\OAuth\P13::requestAccessToken()` is not defined.
Interestingly if I place these new stubs after the $this->getFacebookUserData stub then the error is 'not found' instead of 'not defined'. Clearly I don't fully understand the inner workings at hand :D
Not everything, called methods in your dependencies have to be mocked, because they will in fact be called while testing your classes:
...
$facebookConnect->requestAccessToken($input)->willReturn(<whatever it should return>);
$this->getFacebookUserData($input)->willReturn($returnCurrentUser);
...
If you don't mock them, phpspec will raise a not found.
I'm not familiar with the classes involved but that error implies there is not method Oauth:: requestAccessToken().
Prophecy will not let you stub non-existent methods.

Yii's magic method for controlling all actions under a controller

Commando need's help from you.
I have a controller in Yii:
class PageController extends Controller {
public function actionSOMETHING_MAGIC($pagename) {
// Commando will to rendering,etc from here
}
}
I need some magic method under Yii CController for controlling all subrequest under /page || Page controller.
Is this somehow possible with Yii?
Thanks!
Sure there is. The easiest way is to override the missingAction method.
Here is the default implementation:
public function missingAction($actionID)
{
throw new CHttpException(404,Yii::t('yii','The system is unable to find the requested action "{action}".',
array('{action}'=>$actionID==''?$this->defaultAction:$actionID)));
}
You could simply replace it with e.g.
public function missingAction($actionID)
{
echo 'You are trying to execute action: '.$actionID;
}
In the above, $actionID is what you refer to as $pageName.
A slightly more involved but also more powerful approach would be to override the createAction method instead. Here's the default implementation:
/**
* Creates the action instance based on the action name.
* The action can be either an inline action or an object.
* The latter is created by looking up the action map specified in {#link actions}.
* #param string $actionID ID of the action. If empty, the {#link defaultAction default action} will be used.
* #return CAction the action instance, null if the action does not exist.
* #see actions
*/
public function createAction($actionID)
{
if($actionID==='')
$actionID=$this->defaultAction;
if(method_exists($this,'action'.$actionID) && strcasecmp($actionID,'s')) // we have actions method
return new CInlineAction($this,$actionID);
else
{
$action=$this->createActionFromMap($this->actions(),$actionID,$actionID);
if($action!==null && !method_exists($action,'run'))
throw new CException(Yii::t('yii', 'Action class {class} must implement the "run" method.', array('{class}'=>get_class($action))));
return $action;
}
}
Here for example, you could do something as heavy-handed as
public function createAction($actionID)
{
return new CInlineAction($this, 'commonHandler');
}
public function commonHandler()
{
// This, and only this, will now be called for *all* pages
}
Or you could do something way more elaborate, according to your requirements.
You mean CController or Controller (last one is your extended class) ?
If you extended CController class like this:
class Controller extends CController {
public function beforeAction($pagename) {
//doSomeMagicBeforeEveryPageRequest();
}
}
you could get what you need

Categories