Should Dependencies Be Referenced By Interface Only? - php

I have been working on transitioning from procedural -> oop for a few months now. I decided to avoid jumping right into a framework. I didn't want to rely on the behind the scenes magic, so during my spare time i decided to convert an old project and build a small framework to run it. For tutorials i have mainly turned to googling laravel/laracast tutorials since it is the easiest to find information on. I would say i am at a point where my 'framework' is a smaller simplified ( by this i mean not as feature packed/complex ) version of laravel. I haven't wrapped my head around all of the things symfony/laravel offer but things are starting to click as i progress. At the moment i believe i will be working with laravel once i finish converting my old project.
Ok now. One of the things i continuously have doubts about is dependency injection. All/most injectors that i have found inject via construct using type hinting. I started out with a static registry and slowly iterated until i had the following class
<?php
/**
*------------------------------------------------------------------------------
*
* Framework Container - Class Builder + Injector + Container
*
* The Container Is Responsible For Dependency Injection Via Class Constructors
* Through Abstract 'Alias' Keys Defined Within The Class. It Is Also Used To
* Hold Various Data Including Closures, Or Cached Object Instances.
*
*/
namespace eSportspCMS;
use \eSportspCMS\Helpers\Support\Base;
use \eSportspCMS\Contracts\Container as ContainerContract;
class Container extends Base implements ContainerContract {
/**
* Alias Container
* Contains Alias Keys Defining Classes To Instantiate See $this->make()
*/
private $aliases = [];
/**
* Binding Container
* Contains Custom Closure Bindings See $this->make()
*/
private $bindings = [];
/**
* Cache Container
* Contains Previously Instantiated Classes See $this->make()
*/
private $cache = [];
/**
* Closure Container
* Contains Custom Closures To Avoid Hardcoding Within Class
*/
private $closures = [];
/**
* Class Dependency Key
* Public Var Within Classes Defining The Dependency List
*/
private $varkey = 'dependencies';
/**
* Define Class Dependency Key
*
* #param string $varkey Class Dependency Key
*/
public function setvarkey($varkey) {
$this->varkey = $varkey;
}
/**
* Set Data Within Containers
*
* #param string $key Key To Use When Setting Container Var
* #param mixed $value Value To Use When Setting Container Var
*/
public function alias($key, $value) { $this->aliases[$key] = $value; }
public function bind ($key, $value) { $this->bindings[$key] = $value; }
public function cache($key, $value) { $this->cache[$key] = $value; }
/**
* Add New Closure Within Container
*
* #param string $key Closure Key
* #param callable $value Callable Function
*/
public function closure($key, callable $value) {
if (method_exists($this, $key)) {
throw new \Exception($key . ' Already Exists As A Method. Which
Means This Closure Would Not Be Accessible If Set!');
}
$this->closures[$key] = $value;
}
/**
* Access Closure If Method Being Called Does Not Exist
*
* #param string $key Closure Key
* #param array $params Params To Pass To Closure
* #return mixed Closure Response If Exists
*/
public function __call($key, $params = []) {
if (isset($this->closures[$key]) && is_callable($this->closures[$key])) {
return call_user_func_array($this->closures[$key], $params);
}
return false;
}
/**
* Create New Class Instance
*
* Forces $this->make() To Instantiate A New Class Instead Of Returning
* A Cached Instance.
*
* #see $this->make() Comments
*/
public function makeNew($class, $params = []) {
return $this->make($class, $params, false, true);
}
/**
* Pull Class From Cache Based On Key, Call Binding Or Instantiate Class
* Resolve Dependencies And Pass Via Construct.
*
* #param string $key Alias Key | Binding Key | Class To Make
* #param array $params Additional Params Passed Via Constructor
* #param bool $cache Determines If Class Can Be Cached
* #param bool $new Forces New Instance Of Object
* #return object Instantiated Or Cached Object
*/
public function make($key, $params = [], $cache = true, $new = false) {
/**
* Params Indicate Cached Instance Can Be Used
*/
if (!$new && isset($this->cache[$key])) {
return $this->cache[$key];
}
/**
* If Binding Is Defined And Key Matches Return It Instead Of Building
* The Class Directly. Replace Params With App
*/
if (isset($this->bindings[$key]) && is_callable($this->bindings[$key])) {
$instance = call_user_func_array($this->bindings[$key], $this->make('app'));
$cache ? $this->cache($key, $instance) : '';
return $instance;
}
/**
* Cache And Binding Statement Failed! Attempt To Build Class.
*
* If Class Exists Instantiate, Resolve/Pass Dependencies,
* Cache ( If Allowed ), And Return.
*
* Else Throw Exception!
*/
$classname = isset($this->aliases[$key]) ? $this->aliases[$key] : $key;
if (class_exists($classname)) {
$instance = new $classname($this->resolveDependencies($classname, $params));
$cache ? $this->cache($key, $instance) : '';
return $instance;
}
// All Statements Failed! Class Couldn't Be Created
throw new \Exception('Container Could Not Create Class: ' . $classname);
}
/**
* Resolve/Build Class Dependencies
*
* Dependencies Cascade To Simplify/Unify Dependency Setting Within Grouped
* Classes. ( Classes Like Controllers Which Would Extend Base Controller )
*
* #param string $classname Class Being Instantiated
* #param array $params Additional Params Being Passed
* #return array Assoc Array With Class Dependencies
*/
private function resolveDependencies($classname, $params = []) {
// Define Class Tree
$classes = array_reverse((array) class_parents($classname));
$classes[] = $classname;
// Retrieve Class Dependencies From Tree ( Alias Keys ) & Build
$dependencies = $this->dependencies($classes);
foreach ((array) $dependencies as $dependency) {
$dependencies[$dependency] = $this->make($dependency);
}
// Return Merged Dependencies
return array_merge($dependencies, $params);
}
/**
* Retrieve Class Dependency List ( Alias Keys )
*
* #param array $classes Array Containing Classes
* #return array Class Dependencies ( Alias Keys )
*/
private function dependencies($classes = []) {
if (!$classes) { return; }
$aliases = [];
foreach ((array) $classes as $c) {
$vars = get_class_vars($c);
if (isset($vars[$this->varkey])) {
$aliases = array_merge($aliases, $vars[$this->varkey]);
}
}
return array_unique($aliases);
}
}
How I Use It
The framework application class will extend the container. During application bootstrap the alias keys for dependencies will be loaded ( i use the same configuration setup of laravel - loading config from returned arrays within config directory ) When a class is instantiated using the container it will search for the dependency list 'varkey', it will iterate and merge parent class dependencies ( for grouped dependencies like models - models extend base model ) it will then build the dependency tree, instantiate the class and cache if allowed. The dependencies are passed to the class via construct as an assoc array ['alias' => object]. Classes extending the '\helpers\support\base' class have the dependencies set for them. The base construct class iterates through the array setting the dependency within the class using the alias key as the class key.
Concerns
When looking at other injectors i see typehinting being used and i am not sure if that is due to a flaw with this type of dependency injection that i am not seeing yet. The way i see it the alias keys are the 'interfaces' on top of that the dependencies that are used implement contracts so when i work on changing the class the contract still defines the class requirements.
Questions
Is there a reason to:
* stray away from using this injector? Why? ( I am not looking for "Dont reinvent the wheel" answers i am trying to further my knowledge and understand what i am doing )
* implement service providers "laravel tutorials" when using this? Currently i manipulate dependencies within construct ( if i need specific data from a dependency i use getters/setters to retrieve the data and set within the class )
Do i need to create a contract/interface for EVERY class that i create including controllers, or are interfaces used on classes that are dependencies only? ( I have found lots of mixed feedback so i am torn atm thought i would throw this in )
Class Example
The following class is my 404 error controller. It is one of the dependencies used in other controllers since i have pages dependent on db data ( users ) i display 404 errors if the user does not exist.
<?php
/**
*------------------------------------------------------------------------------
*
* Error Controller
*
*/
namespace Application\Controllers;
use \eSportspCMS\Helpers\Support\Base;
class Error extends Base {
public $dependencies = ['http', 'log', 'metadata', 'view'];
/**
* 404 Error Page Not Found
*/
public function display404($msg = '', $params = []) {
// If Msg Present Log Error
!$msg ?: $this->log->error($msg, $params);
// Define 404 HTTP Header
$this->http->set('header', 'HTTP/1.1 404 Not Found');
// Set 404 Sitetitle
$this->metadata->sitetitle('error', 'display404');
// Return View File + View Data
return $this->view->display('www/custompages', $this->custompage->info(1));
}
}
Thank you in advance for all feedback.

Related

I wonder how this code works: it is related to classes in PHP

I am working with a framework to build a telegram bot, since I am not a pro in PHP, I am trying to understand the logic behind the codes; there is one thing I cannot understand. then, I decided to ask it here.
The code is much bigger; so I cut some parts of it. I have a specific problem with this method. I don't exactly get how they specified a method for getMessage(). It didn't have any functions with this name in this class.
<?php
/**
* Class Command
*
* Base class for commands. It includes some helper methods that can fetch data directly from the Update object.
*
* #method Message getMessage() Optional. New incoming message of any kind — text, photo, sticker, etc.
abstract class Command
{
/**
* Constructor
*
* #param Telegram $telegram
* #param Update|null $update
*/
public function __construct(Telegram $telegram, ?Update $update = null)
{
$this->telegram = $telegram;
if ($update !== null) {
$this->setUpdate($update);
}
$this->config = $telegram->getCommandConfig($this->name);
}
/**
* Set update object
*
* #param Update $update
*
* #return Command
*/
public function setUpdate(Update $update): Command
{
$this->update = $update;
return $this;
}
/**
* Pre-execute command
*
* #return ServerResponse
* #throws TelegramException
*/
public function preExecute(): ServerResponse
{
if ($this->need_mysql && !($this->telegram->isDbEnabled() && DB::isDbConnected())) {
return $this->executeNoDb();
}
if ($this->isPrivateOnly() && $this->removeNonPrivateMessage()) {
$message = $this->getMessage();
if ($user = $message->getFrom()) {
return Request::sendMessage([
'chat_id' => $user->getId(),
'parse_mode' => 'Markdown',
'text' => sprintf(
"/%s command is only available in a private chat.\n(`%s`)",
$this->getName(),
$message->getText()
),
]);
}
return Request::emptyResponse();
}
I just don't get it how this->getMessage() is working. Is there somebody help me out?
This is a base class, and some other class is extending this class. Since this is an abstract class, it cannot be instantiated.
So you need to look for another class that extends this class (i.e class SomeCommand extends Command). The SomeCommand class in this case will declare the getMessage function.
(Note that SomeCommand is an example, I don't know the real name of the other class.)
Edit:
That function was removed in this commit in favor of using the magic method __call instead. No idea why.

PHPDoc return type with extended Factory pattern

I want to use an abstract base class with common functionality for factories to extend, which works, but I don't know how to accurately specify the return type and have it detected by PHPStorm.
Here's an example. Is there a way I can document in PHPDoc that AppleFactory::make() returns AppleInterface and OrangeFactory::make() returns OrangeInterface?
<?php
namespace App\Factories;
abstract class AbstractFactory {
/** #var array $drivers */
protected $drivers;
/**
* instantiate the driver based on the given driver identifier
* #param string $driver Driver identifier.
* #return ???
* #throws UnknownDriverException If driver string is not in list of available drivers.
*/
public function make($driver) {
$class = $this->className($driver);
if (is_null($class))
throw new UnknownDriverException($driver);
return new $class;
}
/**
* get the full class name for the driver
* #param string $driver String mapping of class.
* #return string
*/
public function className($driver) {
return isset($this->drivers[$driver]) ? $this->drivers[$driver] : null;
}
}
class AppleFactory extends AbstractFactory {
protected $drivers = [
// both implement AppleInterface
'fuji' => \App\Apples\Fuji::class,
'gala' => \App\Apples\Gala::class
];
}
class OrangeFactory extends AbstractFactory {
protected $drivers = [
// both implement OrangeInterface
'navel' => \App\Oranges\Navel::class,
'blood' => \App\Oranges\Blood::class
];
}
Is there a way I can document in PHPDoc that AppleFactory::make() returns AppleInterface and OrangeFactory::make() returns OrangeInterface?
Based on your requirements above - a standard #method should do the job -- needs to be placed in PHPDoc comment for that class (AppleFactory and OrangeFactory accordingly). Something like this:
#method AppleInterface make($driver)
At the same time, since you do pass parameter to a factory method .. and returned instance has strong relation to that -- have a look at Advanced Metadata support in PhpStorm (IDE specific functionality). This is what Laravel IDE helper (for example) uses to provide better IDE integration with this framework.
More on this: https://confluence.jetbrains.com/display/PhpStorm/PhpStorm+Advanced+Metadata

Where do I place my lengthy code that prepares domain objects to be persisted in a Repository?

I have some code like so:
/* Part of Controller::saveAction() */
//create new object instance
$item = new Item();
//populate the Item
$item->setDescription($description);
$item->setQuantity($quantity);
$item->setPrice($price);
//once we have a fully populated object,
//send it to Repository pattern,
//which saves it to persistent storage
$this->repository->saveItem($item);
Imagine several blocks like the above in a single funciton/method, and you will see my issue ... I am pretty happy with my repository line of code, but I do not know where to place all the "prep work" that is done before the call to Repository.
Question:
Where do I place the bulk of code that creates &populates the Item object instance? It is cluttering up my Controller method and I can't seem to imagine another place to put it.
Goal
My goal is to find good structure/design rather than to purely reduce or minimize the number of "item preparation" lines.
Depending on Controller, I have roughly 5-7 Item instances that each have 10-16 lines of code creating and populating the instances.
Observation
Individual values like $description, $quantity, $price must come from somewhere. It could be GET, POST, SESSION, COOKIES, or database or external methods. Let's call it $_SOMEWHERE for clarity. We then get:
$description = $_SOMEWHERE['description'];
$quantity = $_SOMEWHERE['quantity'];
$price = $_SOMEWHERE['price'];
Define an Input&Preparation class that does the work for you and returns the prepared Item.
class AcquireItem
{
function getItem()
{
$item = new Item();
$item->setDescription($_SOMEWHERE['description']);
$item->setQuantity($_SOMEWHERE['quantity']);
$item->setPrice($_SOMEWHERE['price']);
return $item;
}
}
Controller
$item = (new AcquireItem())->getItem();
$this->repository->saveItem($item);
Controller becomes shorter, by effectively "stuffing away" the bulky clutter code into a class that is concerned with reading and preparing input, and decluttering the controller. The code has to exist somewhere, but might as well be out of view, and elsewhere.
For different types of Item, you can vary the method, i.e. getItemA(), getItemB().
As you mentioned about the proper way of achieving
so.. how about using delegators and splitting the controller (blocks) into delegators? It terms of terminology it would be the Facade [Adapter]
add the following to ./module/MyModule/config/module.config.php:
'controllers' => array(
'invokables' => array(
'MyModule\CreateController' => 'MyModule\Controller\MyController',
'MyModule\ReadController' => 'MyModule\Controller\MyController',
),
'delegators' => array(
'MyModule\CreateController' => array(
'MyModule\Controller\Delegator\CreateItemDelegatorFactory'
),
'MyModule\ReadController' => array(
'MyModule\Controller\Delegator\ReadItemDelegatorFactory'
),
),
),
'form_elements' => array(
'invokables' => array(
'item_create' => 'MyModule\Form\CreateForm',
),
),
Create Delegator loads form, populates it, validates and tries to save teh data
./module/MyModule/src/MyModule/Controller/Delegator/CreateItemDelegatorFactory.php:
namespace MyModule\Controller\Delegator;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use MyModule\Entity\Item as Entity;
/**
* Class loads the form, checks if form been posted and if is valid.
* If form is valid it tries to save the item with repository service
* Class sets the Form per controller, such solution keeps form
* validation messages
*/
class CreateItemDelegatorFactory implements DelegatorFactoryInterface
{
/**
* Determines name of the form to be loaded with formElementManager
*
* #var string
*/
private $form_name = "item_create";
/**
* Name of repository service. It may be database, api or other
*
* #var string
*/
private $service_repository_name = "service.repository";
public function createDelegatorWithName(
ServiceLocatorInterface $serviceLocator,
$name,
$requestedName,
$callback
) {
// assign serviceManager locally
$parentLocator = $serviceLocator->getServiceLocator();
// assign services locally
$routerService = $parentLocator->get('router');
$requestService = $parentLocator->get('request');
// get repository service
$repositoryService = $parentLocator->get($this->service_repository_name);
// read the CreateForm with formElementManager and bind the Entity
$callback->setForm(
$parentLocator->get('FormElementManager')->get($this->form_name)
);
$entity = new Entity;
$callback->getForm($this->form_name)->bind($entity);
// check if data been posted
if($requestService->isPost()) {
$postData = $requestService->getPost($this->form_name);
$callback->getForm($this->form_name)->setData($postData);
// validate form
if($callback->getForm($this->form_name)->isValid()) {
// form is valid
$repositoryService->saveItem($entity);
}
}
}
}
With above Delegator your controller (MyModule\Controller\MyController) would need the additional property and two methods:
/**
* Holds the form object
* #var object
*/
private $form
public function setForm($form=null)
{
$this->form = $form;
return $this;
}
public function getForm()
{
return $this->form;
}
./module/MyModule/src/MyModule/Controller/Delegator/ReadItemDelegatorFactory.php:
namespace MyModule\Controller\Delegator;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use MyModule\Entity\Item as Entity;
/**
* Creates Delegator which tries read item's id from the (segment type) route
* and read the Item from the repository service
*
*/
class ReadItemDelegatorFactory implements DelegatorFactoryInterface
{
/**
* Item's ID from route
*
* #var string
*/
private $route_identifier = "item_id";
/**
* Name of repository service. It may be database, api or other
*
* #var string
*/
private $service_repository_name = "service.repository";
public function createDelegatorWithName(
ServiceLocatorInterface $serviceLocator,
$name,
$requestedName,
$callback
) {
// assign serviceManager locally
$parentLocator = $serviceLocator->getServiceLocator();
// assign services locally
$routerService = $parentLocator->get('router');
$requestService = $parentLocator->get('request');
// get repository service
$repositoryService = $parentLocator->get($this->service_repository_name);
// get the router match and the item_id
$routerMatch = $routerService->match($requestService);
$itemId = $routerMatch->getParam($this->route_identifier);
// set the data for the target controller
$callback->setItem($repositoryService->readItem($itemId));
return $callback;
}
With above Delegator your controller (MyModule\Controller\MyController) would need the additional property and method:
/**
* Holds the Item object
* #var object \MyModule\Entity\Item
*/
private $item
public function setItem($item=null)
{
$this->item = $item;
return $this;
}
Such a way of controller's use helps for the code to stay DRY and seems to be possible to control the flow. Delegators are being loaded as LIFO, so it is possiblt to preconfigure the controller ($callback) before passing it to another Delegator.
If the ReadController reads the item and the CreateController loads the form it is a short way for UpdateItemDelegator to handle the Item Update task.
'controllers' => array(
'invokables' => array(
'MyModule\UpdateController' => 'MyModule\Controller\MyController',
'delegators' => array(
'MyModule\ReadController' => array(
'MyModule\Controller\Delegator\UpdateItemDelegatorFactory',
'MyModule\Controller\Delegator\CreateItemDelegatorFactory',
'MyModule\Controller\Delegator\ReadItemDelegatorFactory'
),
),
),
Delegators explained:
http://ocramius.github.io/blog/zend-framework-2-delegator-factories-explained/
Edit:
Controller prepared for both Delegators (Create and Read) would look like:
namespace MyModule\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
class CreateController extends AbstractActionController
{
/**
* Holds the Item object
* #var object \MyModule\Entity\Item
*/
private $item
/**
* Holds the form object
* #var mixed null|object
*/
private $form;
/**
* Item's details. It reads item by the `item_id` which is param set in route with same name
* #return \Zend\View\Model\ViewModel
*/
public function readAction()
{
$v = new ViewModel();
// set item, access it in template as `$this->item`
$v->setVariable('item',$this->getItem());
return $v;
}
/**
* The Form preconfigured with CreateItemDelegatorFactory should be av. in template as `$this->form`
* #return \Zend\View\Model\ViewModel
*/
public function createAction()
{
$v = new ViewModel();
// set form, access the in template as `$this->form`
$v->setVariable('form',$this->getForm());
return $v;
}
/**
* Sets the Item object
* #var $item \MyModule\Entity\Item
* #return $this
*/
public function setItem($item=null)
{
$this->item = $item;
return $this;
}
/**
* Gets the Item object
* #return object \MyModule\Entity\Item
*/
public function getItem()
{
return $this->item;
}
public function setForm($form=null)
{
$this->form = $form;
return $this;
}
/**
* Returns form defined in config and CreateItemDelegatorFactory::form_name
* #return \Zend\Form\Form
*/
public function getForm()
{
return $this->form;
}
}
I personally believe that create-methods should go into the repository.
That is because I would expect the repositories to contain all CRUD (create, read, update, delete) methods.
It's just my personal thought on this topic...

Designing a service container: SRP, choosing the right creational pattern and avoiding tight coupling

I'm writing my own implementation of the Laravel Service Container to practice some design patterns and later make a private microframework.
The class looks like this right now:
class Container implements ContainerInterface
{
/**
* Concrete bindings of contracts.
*
* #var array
*/
protected $bindings = [];
/**
* Lists of arguments used for a class instantiation.
*
* #var array
*/
protected $arguments = [];
/**
* Container's storage used to store already built or customly setted objects.
*
* #var array
*/
protected $storage = [];
/**
* Returns an instance of a service
*
* #param $name
* #return object
* #throws \ReflectionException
*/
public function get($name) {
$className = (isset($this->bindings[$name])) ? $this->bindings[$name] : $name;
if (isset($this->storage[$className])) {
return $this->storage[$className];
}
return $this->make($className);
}
/**
* Creates an instance of a class
*
* #param $className
* #return object
* #throws \ReflectionException
*/
public function make($className) {
$refObject = new \ReflectionClass($className);
if (!$refObject->isInstantiable()) {
throw new \ReflectionException("$className is not instantiable");
}
$refConstructor = $refObject->getConstructor();
$refParameters = ($refConstructor) ? $refConstructor->getParameters() : [];
$args = [];
// Iterates over constructor arguments, checks for custom defined parameters
// and builds $args array
foreach ($refParameters as $refParameter) {
$refClass = $refParameter->getClass();
$parameterName = $refParameter->name;
$parameterValue =
isset($this->arguments[$className][$parameterName]) ? $this->arguments[$className][$parameterName]
: (null !== $refClass ? $refClass->name
: ($refParameter->isOptional() ? $refParameter->getDefaultValue()
: null));
// Recursively gets needed objects for a class instantiation
$args[] = ($refClass) ? $this->get($parameterValue)
: $parameterValue;
}
$instance = $refObject->newInstanceArgs($args);
$this->storage[$className] = $instance;
return $instance;
}
/**
* Sets a concrete implementation of a contract
*
* #param $abstract
* #param $concrete
*/
public function bind($abstract, $concrete) {
$this->bindings[$abstract] = $concrete;
}
/**
* Sets arguments used for a class instantiation
*
* #param $className
* #param array $arguments
*/
public function setArguments($className, array $arguments) {
$this->arguments[$className] = $arguments;
}
}
It works fine but I clearly see a violation of SRP in the make() method. So I decided to delegate an object creational logic to a separate class.
A problem that I encountered is that this class will be tightly coupled with a Container class. Because it needs an access to $bindings and $arguments arrays, and the get() method. And even if we pass these parameters to the class, the storage still stays in a container. So basically all architecture is wrong and we need, like, 2 more classes: StorageManager and ClassFactory. Or maybe ClassBuilder? And should ClassFactory be able to build constructor arguments or it needs another class — ArgumentFactory?
What do you think guys?

Strict standards notices View/Layout class

I am geting a headache with php "Strict Standards" notices in a class:
Parent class:
class View {
/**
*
* #param string $viewFolder
* #param string $viewBasename
* #return boolean
*/
public static function exists($viewFolder, $viewBasename){
$exists = false;
if(\is_string($viewFolder) && \is_string($viewBasename)){
$exists = \is_file(\APPLICATION_PATH."View/$viewFolder/$viewBasename.phtml");
}
return $exists;
}
/**
*
* #param string $viewFolder
* #param string $viewBasename
* #param array $viewVariables
*/
public static function load($viewFolder, $viewBasename,array $viewVariables = []){
extract($viewVariables);
require \APPLICATION_PATH."View/$viewFolder/$viewBasename.phtml";
}
}
Child class:
class Layout extends View{
/**
*
* #param string $viewBasename
*/
public static function exists($viewBasename) {
return parent::exists('_layout', $viewBasename);
}
/**
*
* #param string $viewBasename
* #param array $viewVariables
*/
public static function load($viewBasename, array $viewVariables = array()) {
parent::load('_layout', $viewBasename, $viewVariables);
}
}
I've read this topic and now its clear that the reason are those missing parameters in the child class methods.
Declaration of Methods should be Compatible with Parent Methods in PHP
Is there a way of getting rid of these notices without disabling error reporting, or maybe a better approach of doing this?
Thanks in advance.
The better approach is to write your classes in a clean and sensible way. In terms of OOP practice, your child classes that need to extend the parent's methods should redefine them in the same format (hence your warning from PHP).
In your example, your general workflow for the exists() method implementation appears to be the following:
Parent class has an exists method with a folder and a filename
Child class is cutting corners, because it knows its folder already, and only accepts a filename
Child class passes a predefined variable to the parent method
If you look at this objectively, your goal is that the view should be able to call the exists() method on a Layout class and only pass the one parameter, so then you ask "how can I remove the requirement to pass the folder?" Here are a couple of options:
1: Pass the folder name in as the second argument, and making it optional in the Layout (child) class's implementation:
# Class: Layout
/**
* #param string $viewBasename
* #param string $viewFolder
*/
public static function exists($viewBasename, $viewFolder = '_layout') {
return parent::exists($viewBasename, $viewFolder);
}
# Class: View
public static function exists($viewBasename, $viewFolder) {
// essentially you swap around the order of the params
}
2: Don't pass in the folder at all, but use a class property in the child and take advantage of late static bindings:
# Class: Layout
/**
* Define the folder for your layouts
* #var string
*/
const VIEW_FOLDER = '_layout';
The exists() implementation stays the same as in your example currently.
# Class: View
public static function exists($viewBasename) {
// Get your folder from a child instead of an argument
$viewFolder = static::VIEW_FOLDER;
$exists = false;
if(\is_string($viewFolder) && \is_string($viewBasename)){
$exists = \is_file(\APPLICATION_PATH."View/$viewFolder/$viewBasename.phtml");
}
return $exists;
}
A note here, you could also use a function in place of the constant, either abstract in the View class or not, e.g.:
# Class: View
abstract class View {
/**
* This method should be defined in children to provide the layout name.
* Using an abstract method would ensure that it is defined by children,
* however if View is going to be used on its own then do not use this approach.
* #return string The view's folder name
*/
abstract protected static function getViewFolder();
public static function exists($viewBasename) {
// Get view folder from the children (same as the constant example)
$viewFolder = static::getViewFolder();
// ...
}
}
# Class: Layout
class Layout extends View {
protected static function getViewFolder() {
return '_layout';
}
public static function exists($viewBasename) {
return parent::exists($viewBasename);
}
}
To be honest, the constant option is a little shorter and they both essentially do the same thing, other than if you use a function instead of a constant you would be able to define manipulation logic if required.
If I were you, I'd use a class constant for the view folder and take it out as an argument. You'd then implement the static::VIEW_FOLDER in place of the argument passed into load and exists.

Categories