PHP/Laravel magic, how does this work? - php

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.

Related

yii2 correct application architecture (pass data to layout)

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();

How to register filters (formerly helpers) in Latte?

I would like to create my own filter for Latte templating engine. There is an example in their documentation but it doesn't describe how to register it inside presenter.
$latte = new Latte\Engine;
$latte->addFilter('myFilter', function ($s) {
return someMagic($s)
});
I bet there will be simple way to get instance of Latte\Engine inside presenter but I'm not sure how.
Filters can be registered through config.neon too.
services:
nette.latteFactory:
setup:
- addFilter(abs, #App\Latte\AbsFilter)
- App\Latte\AbsFilter
Filter class can look like this:
namespace App\Latte;
class AbsFilter extends \Nette\Object
{
/**
* #param int $number
* #return int
*/
public function __invoke($number)
{
return abs($number);
}
}
In presenter, there is instance of Latte\Engine available in $this->template so everything you have to do is register filter like this:
<?php
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
public function beforeRender()
{
// register filters
$this->template->addFilter('myFilter', function ($s) {
// don't forget to set your own magic
return someMagic($s);
});
}
}
?>
I postend an example using BasePresenter which is parent of all others presenters but you can register it only in presenter you want to and speed up your application.
In addition to #Nortys answer.
Sometimes it's usefull to inject some data from Presenter into anonymous function:
<?php
abstract class BasePresenter extends Nette\Application\UI\Presenter
{
public function beforeRender()
{
$locale = 'en';
// register filters
$this->template->addFilter('myFilter', function ($s) use ($locale) {
// don't forget to set your own magic
return someMagic($s, $locale);
});
}
}
?>
Nette 3.0 / 2020 Approach with Dependency Injection
Registering filters in control or presenter can lead to forgotten registration of filter. They should be registered once, at one place for whole application.
This approach is easy for extension, 1 new filter = 1 new class, no configuration, no touching other code. Apart other answers here, this one respects open-closed solid principle.
LatteFactory service with FilterProvider
First, we make a filter provide service:
interface FilterProviderInterface
{
public function getName(): string;
}
final class PlusFilterProvider implements FilterProviderInterface
{
public function __invoke(int $number, int $anotherNumber): int
{
return SomeStaticClass::plus($number, $anotherNumber);
}
public function getName(): string
{
return 'plus';
}
}
<?php
declare(strict_types=1);
namespace App\Latte;
use Latte\Engine;
use Latte\Runtime\FilterInfo;
final class LatteFactory
{
/**
* #var FilterProviderInterface[]
*/
private array $filterProviders;
/**
* #param FilterProviderInterface[] $filterProviders
*/
public function __construct(array $filterProviders)
{
$this->filterProviders = $filterProviders;
}
public function create(): Engine
{
$engine = new Engine();
// register your filters here
foreach ($this->filterProviders as $filterProvider) {
$engine->addFilter($filterProvider->getName(), $filterProvider);
}
return $engine;
}
}
How to add a new filter?
create new class that implements your interface FilterProviderInterface
register is as a service in your config, or better autodiscover it with search extension
that's it :)
Do you want to learn more?
I wrote more detail post How to Get Rid of Magic, Static and Chaos from Latte Filters where I explain the pros and cons of alternatives and why this one wins.

Cannot set layout via constructor

I tried almost everything but I just cant get the following to run.
<?php
class BaseController extends Controller {
// Define frontpage layout manager
protected $layout = '';
public function __construct() {
parent::__construct();
$theme = Theme::where('enabled', '=', true)->first();
// HERE !! : This never changes the value of $layout class var
$this->layout = View::make('themes.front.' . $theme->folder . '.master');
// I also tried without View::make(..)
// I also checked that $theme returns something and it does return a theme
}
/**
* Setup the layout used by the controller.
*
* #return void
*/
protected function setupLayout()
{
if ( ! is_null($this->layout))
{
$this->layout = View::make($this->layout);
}
}
}
I simply cannot change the value of $layout in my constructor. I need this to allow user to switch between layouts.
So what I wanted to achieve with this is: I would have multiple layouts (templates) and I would allow user to change these templates via administration so I needed a quick and easy way to manipulate the protected $layout value.
The problem with putting my code in __constructor() {} is that setupLayout() will override it and thus the error as no layout is found.
So there are two solutions:
1) Declare layout in each sub-controller
Meaning each controller that extends your base controller defines it's own protected $layout in it's own __constructor() {} method. However this is very repetitive if all your pages share the same template.
2) Maniuplate the setupLayout() method
Since all my pages share the same layout and I know for certian there will always be at least one template I was able to simply alter the setupLayout() method to:
function setupLayout()
{
$theme = Theme::where('enabled', '=', true)->first();
$this->layout = 'themes.front.' . $theme->folder . '.master';
}

Help getting Model into my Controllers, with MVC

I have been working on my own library/framework for the learning experience for a while. MVC is one of those things that took me a while to really understand but I do finally "Get it".
Below is some sample code for a basic MVC setup in PHP. I think I am in the right direction so far, where I need a little help is down in the "Example controller" near the bottom, you will see where I can create a view, I just need to figure out how to best get my data from a model file into that controller class. Please help with example code if you can, hopefully I am making sense.
Also I am welcome to any comments/suggestions on any of the code
Abstract Controller class...
/**
* MVC Example Project
*/
/**
* Extend this class with your Controllers
* Reference to the model wrapper / loader functions via $this->model
* Reference to the view functions via $this->view
*/
abstract class Core_Controller {
protected $view;
protected $model;
function __construct($dependencyContainer){
$this->view = new Core_View();
//$this->view = $dependencyContainer->get(view);
}
}
Abstract Model class...
/**
* Extend this class with your models and reference to the database object via $this->$db
*/
abstract class Core_Model {
protected $db;
protected $session;
function __construct($dependencyContainer) {
$this->db = $dependencyContainer->get(database);
$this->session = $dependencyContainer->get(session);
}
}
View class, might make it abstract as well...
class Core_View {
protected $data;
# Load a view file (views/$view.php);
# $param data this gets extracted and be thus be used inside the view
# When loading another view from inside the view file the data is 'cached' so you
# don't have to pass them again
public function load($view,$data = null) {
if($data) {
$this->data = $data;
extract($data);
} elseif($this->data != null) {
extract($this->data);
}
require(APP_PATH . "Views/$view.php");
}
public function set($data = null) {
if($data) {
$this->data = $data;
extract($data);
} elseif($this->data != null) {
extract($this->data);
}
}
}
Example putting it together...
/**
* Example Controller
*/
class User_Controller extends Core_Controller {
public function profile()
{
$profileData = array();
$profileData = //GET from Model
$this->view->load('userProfile', $profileData);
}
}
?>
My suggestion is not to tie view and model to the controller at all. Let them be instantiable from controller code, just like any other classes. You can then get the model data (and pass it to the view) in standard object oriented way.
Will you use a Data access layer (DAL) / Object-relational mapping (ORM)? Take a look at Zend_Db, Doctrine or Propel
I'd say that you're missing the part of the application that manipulate your models. It could be your controller, but isn't a good practice. So we need a model mapper.
The best way to get model data from your controller is simply calling it. But generally we use a kind of "pointer" which knows how to populate your object model. This pointer is called "Mappers" (Data Mapper Pattern):
$MyModelMapper = new MyModelMapper();
$Profile = $MyModelMapper->getProfileById($id); // return Core_Model.
This function will perform a database query and will populate one specific model with the data. You could also get an array of objects for a "list" action for example.
Then you'll pass this model to your view.
I think you should take a look at the Zend Framewok quick start. It will give you some ideas.
See this question too: What's the difference between DAO and Data Mapper

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