I started to wonder about what exactly is the purpose of service providers in Laravel, and why they work in the way they do. After searching through some articles,
the key points of service providers in my understanding are:
Simplifies object creation (Laravel What is the use of service providers for laravel)
Decoupling your code (r/laravel: When to use service providers?)
Dependency injection
Reduces technical debt
So it basically binds an implementation to an interface, and we can use it by
$app(MyInterface::class)
or something like that, and we can just change the implementation when needed, only in one place, and the rest of our code which depends on it won't break.
But i still can not grasp the concept, why they are the way they are, it seems overcomplicated. I peaked in to the code, it was certainly a ton of work to make Service Providers & Containers work, so there must be a good reason.
So to learn further, i tried to make my own, more simple version of it, which achieves the same goals. (i obviously lack a lot of info on this, and most probably missed some other goals)
My question is, why would this implementation would not satisfy the same use cases?
Service.php
namespace MyVendor;
/**
* Abstract class for creating services
*/
abstract class Service
{
/**
* Holds the instance of the provided service
*
* #var mixed
*/
private static mixed $instance = null;
/**
* Retrieves the instance of the provided service & creates it on-demand
*
* #return mixed
*/
public static function get(): mixed
{
if (self::$instance === null) {
self::$instance = static::instantiate();
}
return self::$instance;
}
/**
* A function which contains the service's object creation logic
*
* #return mixed
*/
abstract protected static function instantiate(): mixed;
}
Example implementation:
For the example, i chose an interface to parse environment variables, as i already had phpdotenv in my project as a dependency
Services/DotenvParser/DotenvParserInterface.php
namespace MyVendor\Services\DotenvParser;
/**
* This is the service interface i want to provide
*/
interface DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array;
}
Now i will have 2 implementations of this class. I will pretend that a lot of my code already depends on DotenvParserInterface. An old, hacky one which "depends" on another thing, and the replacement for it which uses phpdotenv
A quick fake dependency:
Services/DotenvParser/Dependency.php
namespace MyVendor\Services\DotenvParser;
class Dependency
{
private bool $squeeze;
public string $bar;
public function __construct(string $foo, bool $squeeze)
{
$this->squeeze = $squeeze;
$this->bar = $foo;
if($this->squeeze){
$this->bar .= " JUICE";
}
}
}
Our old code:
Services/DotenvParser/OldDotenvCode.php
namespace MyVendor\Services\DotenvParser;
use BadMethodCallException;
use InvalidArgumentException;
class OldDotenvCode implements DotenvParserInterface
{
/**
* Our fake dependency
*
* #var Dependency
*/
private Dependency $foo;
private string $dir;
private string $fileName;
private string $contents;
private array $result;
public function __construct(Dependency $myDependency)
{
$this->foo = $myDependency;
}
/**
* Implementation of DotenvParserInterface
*
* #param string $directory
* #param string $fileName
* #return array
*/
public function parse(string $directory, string $fileName = ".env"): array
{
try{
$this->setDir($directory)->setFileName($fileName);
}catch(BadMethodCallException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$this->getEnvContents();
$this->contents = $this->getEnvContents();
$this->result = [];
foreach(explode("\n", $this->contents) as $line){
$exploded = explode("=", $line);
$key = $exploded[0];
$value = (isset($exploded[1])) ? trim($exploded[1], "\r") : "";
if($this->foo->bar === "ORANGE JUICE"){
$value = trim($value, "\"");
}
$this->result[$key] = $value;
}
return $this->result;
}
#region Old, bad stuff
public function setDir(string $directory): self{
if(!\is_dir($directory)){
throw new InvalidArgumentException("Directory $directory is not a valid directory");
}
$this->dir = rtrim($directory, "/");
return $this;
}
public function setFileName(string $fileName): self{
if(empty($this->dir)){
throw new BadMethodCallException("Must call method setDir() first with a valid directory path");
}
$fileName = ltrim($fileName, "/");
if(!\file_exists($this->dir . "/" . $fileName)){
throw new InvalidArgumentException("File $fileName does not exist in provided directory {$this->dir}");
}
$this->fileName = $fileName;
return $this;
}
private function getFilePath(): string{
if(empty($this->fileName)){
throw new BadMethodCallException("Must call method setFileName() first");
}
return $this->dir . "/" . $this->fileName;
}
private function getEnvContents(): string{
return \file_get_contents($this->getFilePath());
}
public function setup(): void
{
$this->setDir($directory)->setFileName($fileName);
}
#endregion
}
Now, the phpdotenv version
Services/DotenvParser/phpdotenv.php
namespace MyVendor\Services\DotenvParser;
use Dotenv\Dotenv;
use InvalidArgumentException;
use Dotenv\Dotenv;
use InvalidArgumentException;
class phpdotenv implements DotenvParserInterface
{
public function parse(string $directory, string $fileName = ".env"): array
{
try{
Dotenv::createMutable($directory, $fileName)->load();
}catch(\Dotenv\Exception\InvalidPathException $e){
throw new InvalidArgumentException($e->getMessage(), 0, $e);
}
$result = $_ENV;
$_ENV = []; //Hehe
return $result;
}
}
Our service which we made from extending our Service class
Services/DotenvParser/DotenvParserService.php
namespace MyVendor\Services\DotenvParser;
use MyVendor\Service;
class DotenvParserService extends Service
{
// We can do this to make type hinting for ourselves
public static function get(): DotenvParserInterface
{
return parent::get();
}
protected static function instantiate(): DotenvParserInterface
{
$year = 2022;
// Some condition, to return one or another
if($year < 2022){
$dep = new \MyVendor\Services\DotenvParser\Dependency("ORANGE", true);
return new OldDotenvCode($dep);
}
return new phpdotenv();
}
}
And now, we can use it like this:
$dotenvparser = \MyVendor\Services\DotenvParser\DotenvParserService::get();
$result = $dotenvparser->parse(__DIR__);
var_dump($result);
// Outputs an array of our environment variables, yey!
We can also write tests for our services to see if anything breaks:
namespace MyVendorTest\Services\DotenvParser;
use InvalidArgumentException;
use MyVendor\Services\DotenvParser\DotenvParserInterface;
use MyVendor\Services\DotenvParser\DotenvParserService;
final class DotenvParserServiceTest extends \PHPUnit\Framework\TestCase
{
public function doesInstantiate(): void
{
$testParser = DotenvParserService::get();
$this->assertInstanceOf(DotenvParserInterface::class, $testParser);
}
public function testWorksFromValidDirNFile(): void
{
// The actual contents of a .env file
$testArray = [
"DEV_MODE" => "TRUE",
"BASE_HREF" => "http://localhost:8080/"
];
$testParser = DotenvParserService::get();
// phpdotenv loads every parent .env too and i was having none of it for this quick demonstration
$result = $testParser->parse(__DIR__."/../../../", ".env");
$this->assertEquals($testArray, $result);
}
public function testSetupFromInvalidDir(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse("i_am_a_dir_which_does_not_exist");
}
public function testSetupFromInvalidFile(): void
{
$this->expectException(InvalidArgumentException::class);
$testParser = DotenvParserService::get();
$testParser->parse(__DIR__, ".notenv");
}
}
So this ended up quite lenghty, but after having that Service class, you basically only need: An interface, at least one implementation of that interface, and a service class which instantiates an implementation of that interface, and optionally some tests for it. And, you can even do dependency injection with it (??) (circular dependencies would get us stuck in an endless loop), like this:
protected static function instantiate(): FooInterface
{
//BarService & AcmeService are extending our Service class
return new FooInterface(BarService::get(), AcmeService::get(), "ORANGE JUICE")
}
I am ready to absorb massive amounts of information
What other things Laravel's Service providers & containers do than i am aware of?
Why and how is it better than a simpler version, like this one?
Does my version really achieve at least those 4 key points i mentioned in the start?
Related
I have a unit test class in which I want to instantiate a object from another class in order to that I used setUpBeforeClass() fixtures of phpunit. So if I will use that recently instantiated object directly in test function then its working fine.
If i'll use this object into another function which had been created for data providers. So that object sets to null cause providers always execute first.
Is there a way to call dataProviders just before the test runs, instead?
require_once('Dashboard.php');
Class Someclass extends PHPUnit_Framework_TestCase {
protected static $_dashboard;
public static function setUpBeforeClass()
{
self::$_dashboard = new Dashboard();
self::$_dashboard->set_class_type('Member');
}
/**
* Test Org Thumb Image Existense
* param org profile image : array
* #dataProvider getOrgProfileImages
*/
public function testFieldValidation($a,$b){
//If I call that object function here it will give the result.
//$members = self::$_dashboard->get_members();
//var_dump($members); Printing result as expected
$this->assertTrue(true);
}
public function getOrgProfileImages() : array {
//var_dump(self::$_dashboard);
$members = self::$_dashboard->get_members();
$tmp_array = ['2','2'];
return $tmp_array;
}
public static function tearDownAfterClass()
{
self::$_dashboard = null;
}
}
Error:
The data provider specified for Someclass::testFieldValidation is invalid.
Call to a member function get_members() on null
Please help to mitigate this issue.
Note: since I don't have the source of your Dashboard class, I'm using a random number in the examples below instead
Providers are invoked before any tests are run (and before any hooks, including beforeClass have a chance to run). By far the easiest way to achieve what you're after is to populate that static property on the class load:
use PHPUnit\Framework\TestCase;
/** #runTestsInSeparateProcesses enabled */
class SomeTest extends TestCase
{
public static $_rand = null;
public function provider()
{
$rand = self::$_rand;
var_dump(__METHOD__, getmypid(), 'provided rand', $rand);
return ['rand' => [$rand]];
}
/** #dataProvider provider */
public function testSomething($rand)
{
$this->expectNotToPerformAssertions();
var_dump(__METHOD__, getmypid(), 'tested with', $rand);
}
/** #dataProvider provider */
public function testSomethingElse($rand)
{
$this->expectNotToPerformAssertions();
var_dump(__METHOD__, getmypid(), 'tested with', $rand);
}
}
// this runs before anything happens to the test case class
// even before providers are invoked
SomeTest::$_rand = rand();
Or you could instantiate you dashboard in the provider itself, on the first call:
public function provider()
{
// Instantiate once
if (null === self::$_rand) {
self::$_rand = rand();
}
$rand = self::$_rand;
var_dump(__METHOD__, getmypid(), 'provided rand', $rand);
return ['rand' => [$rand]];
}
#dirk-scholten is right. You SHOULD be creating a new object for each test. It's a GOOD testing practice. Frankly it looks more like you are testing the data and not testing the code, which is fine I guess, it's just not the typical use of PHPUnit. Based on the assumption that you want to make sure every user in the database has a thumbnail image (just guessing), I would go with the following:
<?php
class DashboardDataTest extends PHPUnit\Framework\TestCase {
private $dashboard;
public function setUp() {
$this->dashboard = new Dashboard();
}
/**
* Test Org Thumb Image Existence
* param org profile image : array
*
* #dataProvider getOrgProfileImages
*
* #param int $user_id
*/
public function testThumbnailImageExists(int $user_id){
$thumbnail = $this->dashboard->get_member_thumbnail($user_id);
$this->assertNotNull($thumbnail);
}
public function geOrgUserIDs() : array {
$dashboard = new Dashboard();
// Something that is slow
$user_ids = $dashboard->get_all_the_member_user_ids();
$data = [];
foreach($user_ids as $user_id){
$data[] = [$user_id];
}
return $data;
}
}
Each data provider will get called once and only once before the tests. You do not need a static data fixture on the class because phpunit handles the data fixture for you when you use data providers.
Alright so I'm converting a small laravel project to symfony (will get bigger, and the bundling architecture symfony uses will be ideal)
I'm apparently spoiled with laravels facades and eloquent working with existing databases almost right out of the box.
I can't find the most appropriate way to have a wrapper or "helper" class get access to an entities repository.
first let me give a few examples then I will explain what I have attempted. (I'm willing to bounty some points for a good answer but unfortunately the time constraints on the project can't exactly wait)
So in laravel I had all my model classes. Then I created some wrapper / helper classes that would essentially turn the data into something a little more usable (i.e. multiple queries and objects containing more versatile information to work with). And with the magic of facades I could call upon each model and query them without and dependencies injected into these "Helper" classes. keeping them very lean. In symfony it appears the ideal solution is to put all of your reusable database logic in repositories, ok.
In symfony I'm surrounded by Inversion of Control (IoC); which is fine but design pattern is failing to be intuitive for me to fully figure this scenario out. I have tried to create services out every single repository, which works great if being called from a controller or other Dependency Injected (DI) service. But in a standard php class, it appears my hands are tied without passing entity manager to each helper class's constructor. *shivers*
The first limitation is I have zero ability to change the schema of the existing tables (which obviously doesn't change the problem, just don't want anyone to suggest altering the entities).
So how does one accomplish this.
EDIT:
so thanks to #mojo's comment I've pulled off what I wanted to do. Still looking for a better alternative if it exists. (see edit 2 below)
currently I have:
config.yml docterine.orm.entity_managers:
entity_managers:
default:
auto_mapping: true
connection: default
asterisk:
connection: asterisk
mappings:
AsteriskDbBundle: ~
asteriskcdr:
connection: asteriskcdr
mappings:
AsteriskCdrDbBundle:
service.yml
services:
app.services.doctrine.entitymanager.provider:
class: AppBundle\Services\EntityManagerProvider
arguments: [#doctrine]
tags:
- {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}
EntityManagerProvider
namespace AppBundle\Services;
use Doctrine\Bundle\DoctrineBundle\Registry as DoctrineRegistry;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Config\Definition\Exception\Exception;
class EntityManagerProvider
{
/** #var DoctrineRegistry */
private static $doctrine;
public function __construct(DoctrineRegistry $doctrine)
{
static::$doctrine = $doctrine;
}
/**
* #param $class
* #return EntityManager
*/
public static function getEntityManager($class)
{
if(($em = static::$doctrine->getManagerForClass($class)) instanceof EntityManager == false)
throw new Exception(get_class($em) . ' is not an instance of ' . EntityManager::class);
return $em;
}
// oh man does this feel dirty
public function onKernelRequest($event)
{
return;
}
}
Example Controller
$extension = Extension::createFromDevice(DeviceRepository::findById(92681));
ExtendedEntityRepository
namespace AppBundle\Entity;
use AppBundle\Services\EntityManagerProvider;
use AppBundle\Utils\DateTimeRange;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Config\Definition\Exception\Exception;
class ExtendedEntityRepository extends \Doctrine\ORM\EntityRepository
{
/** #var ExtendedEntityRepository */
protected static $instance;
public function __construct(EntityManager $entityManager, ClassMetadata $class)
{
parent::__construct($entityManager, $class);
if(static::$instance instanceof static == false)
static::$instance = $this;
}
// some horribly dirty magic to get the entity that belongs to this repo... which requires the repos to have the same name and exist one directory down in a 'Repositories' folder
public static function getInstance()
{
if(static::$instance instanceof static == false) {
preg_match('/^(.*?)Repositories\\\([A-Za-z_]*?)Repository$/', static::class, $match);
$class = $match[1] . $match[2];
$em = EntityManagerProvider::getEntityManager($class);
static::$instance = new static($em, $em->getClassMetadata($class));
}
return static::$instance;
}
public static function findById($id)
{
return static::getInstance()->find($id);
}
public static function getQueryBuilder()
{
return static::getInstance()->getEntityManager()->createQueryBuilder();
}
public static function getPreBuiltQueryBuilder()
{
return static::getQueryBuilder()->select('o')->from(static::getInstance()->getClassName(), 'o');
}
public static function findByColumn($column, $value)
{
//if($this->getClassMetadata()->hasField($column) == false)
// throw new Exception($this->getEntityName() . " does not contain a field named `{$column}`");
return static::getPreBuiltQueryBuilder()->where("{$column} = ?1")->setParameter(1, $value)->getQuery()->execute();
}
public static function filterByDateTimeRange($column, DateTimeRange $dateTimeRange, QueryBuilder $queryBuilder = null)
{
if($queryBuilder == null)
$queryBuilder = static::getPreBuiltQueryBuilder();
if($dateTimeRange != null && $dateTimeRange->start instanceof \DateTime && $dateTimeRange->end instanceof \DateTime) {
return $queryBuilder->andWhere(
$queryBuilder->expr()->between($column, ':dateTimeFrom', ':dateTimeTo')
)->setParameters(['dateTimeFrom' => $dateTimeRange->start, 'dateTimeTo' => $dateTimeRange->end]);
}
return $queryBuilder;
}
}
DeviceRepository
namespace Asterisk\DbBundle\Entity\Repositories;
use AppBundle\Entity\ExtendedEntityRepository;
/**
* DeviceRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class DeviceRepository extends ExtendedEntityRepository
{
//empty as it only needs to extend the ExtendedEntityRepository class
}
Extension
namespace AppBundle\Wrappers;
use Asterisk\DbBundle\Entity\Device;
class Extension
{
public $displayName;
public $number;
public function __construct($number, $displayName = "")
{
$this->number = $number;
$this->displayName = $displayName;
}
public static function createFromDevice(Device $device)
{
return new Extension($device->getUser(), $device->getDescription());
}
}
Agent (This is an example of why having repositories access statically is helpful)
namespace AppBundle\Wrappers;
use AppBundle\Utils\DateTimeRange;
use Asterisk\CdrDbBundle\Entity\Cdr;
use Asterisk\CdrDbBundle\Entity\Repositories\CdrRepository;
use Asterisk\DbBundle\Entity\Device;
use Asterisk\DbBundle\Entity\Repositories\FeatureCodeRepository;
use Asterisk\DbBundle\Entity\Repositories\QueueDetailRepository;
use Asterisk\DbBundle\Enums\QueueDetailKeyword;
class Agent
{
public $name;
public $extension;
/** #var Call[] */
public $calls = [];
/** #var array|Queue[] */
public $queues = [];
/** #var AgentStats */
public $stats;
private $_extension;
public function __construct(Device $extension, DateTimeRange $dateTimeRange = null)
{
$this->_extension = $extension;
$this->extension = Extension::createFromDevice($extension);
$this->name = $this->extension->displayName;
$this->calls = $this->getCalls($dateTimeRange);
$this->stats = new AgentStats($this, $dateTimeRange);
}
public function getCalls(DateTimeRange $dateTimeRange = null)
{
/** #var CdrRepository $cdrRepo */
$cdrRepo = CdrRepository::getPreBuiltQueryBuilder();
$query = $cdrRepo->excludeNoAnswer($cdrRepo->filterByDateTimeRange($dateTimeRange));
$cdrs = $query->andWhere(
$query->expr()->orX(
$query->expr()->eq('src', $this->extension->number),
$query->expr()->eq('dst', $this->extension->number)
)
)->andWhere(
$query->expr()->notLike('dst', '*%')
)
->getQuery()->execute();
foreach($cdrs as $cdr) {
$this->calls[] = new Call($cdr);
}
return $this->calls;
}
public function getBusyRange(DateTimeRange $dateTimeRange = null)
{
$on = FeatureCodeRepository::getDndActivate();
$off = FeatureCodeRepository::getDndDeactivate();
$toggle = FeatureCodeRepository::getDndToggle();
$query = CdrRepository::filterByDateTimeRange($dateTimeRange);
/** #var Cdr[] $dndCdrs */
$dndCdrs = $query->where(
$query->expr()->in('dst', [$on, $off, $toggle])
)
->where(
$query->expr()->eq('src', $this->extension->number)
)->getQuery()->execute();
$totalTimeBusy = 0;
/** #var \DateTime $lastMarkedBusy */
$lastMarkedBusy = null;
foreach($dndCdrs as $cdr) {
switch($cdr->getDst())
{
case $on:
$lastMarkedBusy = $cdr->getDateTime();
break;
case $off:
if($lastMarkedBusy != null)
$totalTimeBusy += $lastMarkedBusy->diff($cdr->getDateTime());
$lastMarkedBusy = null;
break;
case $toggle:
if($lastMarkedBusy == null) {
$lastMarkedBusy = $cdr->getDateTime();
}
else
{
$totalTimeBusy += $lastMarkedBusy->diff($cdr->getDateTime());
$lastMarkedBusy = null;
}
break;
}
}
return $totalTimeBusy;
}
public function getQueues()
{
$query = QueueDetailRepository::getPreBuiltQueryBuilder();
$queues = $query->where(
$query->expr()->eq('keyword', QueueDetailKeyword::Member)
)->where(
$query->expr()->like('data', 'Local/'.$this->extension->number.'%')
)->getQuery()->execute();
foreach($queues as $queue)
$this->queues[] = Queue::createFromQueueConfig(QueueDetailRepository::findByColumn('extension', $queue->id), $queue);
return $this->queues;
}
}
EDIT 2:
Actually I forgot I declared each repository as a service, so I could omit the black magic voodoo in the getInstance() method. But loading the service on kernel event seems like a bad idea...
parameters:
entity.device: Asterisk\DbBundle\Entity\Device
services:
asterisk.repository.device:
class: Asterisk\DbBundle\Entity\Repositories\DeviceRepository
factory: ["#doctrine.orm.asterisk_entity_manager", getRepository]
arguments:
- %entity.device%
tags:
- {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}
Edit 3
Cerad gave me an answer on my other related question That suggested using a single kernel event listener service and injecting each repository as a dependency. Thus allowing me to access the repositories statically. My only concern is the overhead required to load each repository on every request. My ideal method would be lazy load the repositories, but I'm unaware of a method at this time. proxy-manager-bridge looked promising but with my singleton pattern I don't think it will work.
If I get the behaviour of SPL Autoloader correctly, it acts in a "global scope", right? If we have, say, this code:
class Autoloader {
protected static $instance = NULL;
public static function get_instance() {
NULL === self::$instance and self::$instance = new self;
return self::$instance;
}
public function init() {
spl_autoload_register(array($this,'autoload'));
}
private function autoload() {
// to the autoload magic
}
}
$autoloader = new Autoloader();
$autoloader->init();
// or
add_action('muplugins_loaded', array(Autoloader::get_instance(),'init'));
... it applies to the rest of the application OR if hooked to Wordpress action, from the hook onwards, right?
It just seems to me that it is not very convenient, especially if you work in the frame of larger frameworks (such as Wordpress). Is there way, how to limit the scope of SPL Autoload to specific context by:
defining the SPL Autoload different way?
having something specific in the function (such as return false on e.g. classes which names does not much some sort of pattern)?
something more clever?
I know, that I can add some conditional statements to autoload() function to avoid conflicts and errors, it just does not seem very efficient.
Thanks! And yes, it is very possible that I'm just overlooking something.
Edit
Mimicing directory structure by namespaces: It's not actually possible within Wordpress, is it? If what you're after is some sort of communication and logical structure between, say, must-use plugins and themes. (#RoyalBg)
Not finding a file: How to determine than when it is "okay" to not finding a file and when it is not? By logic of the function? It still seems to me, that reducing the scope of autoloader would be way more elegant sollution. (#Jon, #Mark Baker)
But if I understand your comments well, SPL Autoloader truly applies "globally" by default.
You can register multiple autoloader function/methods. SPL will go through them all until the desired class is loaded.
<?php
function loaderA($name) { // Do Stuff }
function loaderB($name) { // Do Other Stuff }
spl_autoload_register('functionA');
spl_autoload_register('functionB');
?>
SPL will go through these registered functions, one after the other. At first functionA, then functionB, if the requested class is not loaded via functionA.
EDIT:
final class LoaderException extends \Exception{}
class Loader {
/**
* Project-Root-Directory
* #var string
*/
protected static $AbsPath;
/**
* Directory-List
* #var array
*/
protected static $DirList;
/**
* DO NOT INSTANTIATE
*/
private function __construct() {}
/**
* The actual autoloader.
* #param string $name Class-Name
* #return void
* #throws LoaderException
*/
public static function load($name) {
if(!is_string($name))
throw new \InvalidArgumentException('Argument is not a string.');
$a = isset(static::$AbsPath) ? static::$AbsPath : '';
$f = str_replace('\\', '/', $name).'.php';
if(!is_array(static::$DirList)) {
if(file_exists($a.$f))
return include $a.$f;
if(file_exists(getcwd().$f))
return include getcwd().$f;
throw new LoaderException('Unable to load "'.$name.'".');
}
foreach(static::$DirList as $d) {
if(file_exists($a.$d.$f))
return include $a.$d.$f;
}
throw new LoaderException('Unable to load "'.$name.'".');
}
/**
* Registers the Loader.
*/
public static function register() {
spl_autoload_register(__CLASS__.'::load');
}
/**
* Unregisters the Loader.
*/
public static function unregister() {
spl_autoload_unregister(__CLASS__.'::load');
}
/**
* Adds one, or more, directories to the Directory-List.
* #throws LoaderException
*/
public static function addDir() {
foreach(func_get_args() as $k => $v) {
if(!is_string($v))
throw new \InvalidArgumentException('Argument #'.($k+1).' is not a string.');
if(!is_dir($v))
throw new \InvalidArgumentException('Argument #'.($k+1).' is not a directory.');
if(!is_array(static::$DirList) or !in_array($v, static::$DirList))
static::$DirList[] = $v;
}
}
/**
* Removes one, or more, directories from the Directory-List.
* #throws LoaderException
*/
public static function removeDir() {
foreach(func_get_args() as $k => $v) {
if(!is_string($v))
throw new \InvalidArgumentException('Argument #'.($k+1).' is not a string.');
if(in_array($v, static::$DirList))
unset(static::$DirList[array_search($v, static::$DirList)]);
}
}
/**
* Sets the Absolute-Path for the loader, this will speed up the loading process.
* #param string $path Absolute-Path to the Project root directory
* #throws LoaderException
*/
public static function setAbsPath($path = null) {
if(isset($path)) {
if(!is_string($path))
throw new \InvalidArgumentException('Argument is not a string.');
if(!is_dir($path))
throw new \InvalidArgumentException('Invalid path "'.$path.'".');
}
static::$AbsPath = $path;
}
}
This is my basic autoloader i use very often, it recognizes namespaces as subdirectories and can be fed with specific directories where to search for classes.
I'm not sure if I can do that, and if I should do that too. I'm writing some tests that could have the same data provider (IP addresses or integers).
class LocalIpAddressTest extends \PHPUnit_Framework_TestCase
{
protected $parser = null;
protected function setUp()
{
$this->parser = new ApacheLogParser();
$this->parser->setFormat('%A');
}
protected function tearDown()
{
$this->parser = null;
}
/**
* #dataProvider successProvider
*/
public function testSuccess($line)
{
$entry = $this->parser->parse($line);
$this->assertEquals($line, $entry->localIp);
}
/**
* #expectedException \Kassner\ApacheLogParser\FormatException
* #dataProvider invalidProvider
*/
public function testInvalid($line)
{
$this->parser->parse($line);
}
public function successProvider()
{
return array(
array('192.168.1.1'),
array('192.168.001.01'),
array('172.16.0.1'),
array('192.168.0.255'),
array('8.8.8.8'),
// not sure about those 2. They are valid ip-format, but can't be assigned as server address
array('0.0.0.0'),
array('255.255.255.255'),
);
}
public function invalidProvider()
{
return array(
// over 255
array('192.168.1.256'),
array('256.256.256.256'),
array('321.432.543.654'),
// incomplete
array('192.168.1.'),
array('192.168.1'),
array('192.168.'),
array('192.168'),
array('192.'),
array('192'),
array(''),
// malformed
array('1921.68.1.1'),
array('192.681.1.'),
array('.1921.68.1.1'),
array('....'),
array('1.9.2.'),
array('192.168.1.1/24'),
// letters (it' not supporting IPv6 yet...)
array('abc'),
array('192.168.1.x'),
array('insert-ip-address-here'),
array('a.b.c.d'),
);
}
}
Then, I have to test when I use $this->parser->setFormat('%a'), that also receives IP Address as an argument. In this case, I'm duplicating all the code just to change one single line. Is it supposed to be that way? Have some way to reuse these Data Providers?
I think you should be able to do that without any problems as long as the dataprovider method is part of the same class. You could include it in a abstract testcase from which your testcase inherits or make use of traits as of php 5.4+.
I have a Zend Framework application based on the quick-start setup.
I've gotten the demos working and am now at the point of instantiating a new model class to do some real work. In my controller I want to pass a configuration parameter (specified in the application.ini) to my model constructor, something like this:
class My_UserController extends Zend_Controller_Action
{
public function indexAction()
{
$options = $this->getFrontController()->getParam('bootstrap')->getApplication()->getOptions();
$manager = new My_Model_Manager($options['my']);
$this->view->items = $manager->getItems();
}
}
The example above does allow access to the options, but seems extremely round-about. Is there a better way to access the configuration?
I always add the following init-method to my bootstrap to pass the configuration into the registry.
protected function _initConfig()
{
$config = new Zend_Config($this->getOptions(), true);
Zend_Registry::set('config', $config);
return $config;
}
This will shorten your code a little bit:
class My_UserController extends Zend_Controller_Action
{
public function indexAction()
{
$manager = new My_Model_Manager(Zend_Registry::get('config')->my);
$this->view->items = $manager->getItems();
}
}
Since version 1.8 you can use the below code in your Controller:
$my = $this->getInvokeArg('bootstrap')->getOption('my');
Alternatively, instead of using Zend_Registry you could also create a singleton Application class that will contain all application info, with public member functions that allow you to access the relevant data. Below you can find a snippet with relevant code (it won't run as is, just to give you an idea how it can be implemented) :
final class Application
{
/**
* #var Zend_Config
*/
private $config = null;
/**
* #var Application
*/
private static $application;
// snip
/**
* #return Zend_Config
*/
public function getConfig()
{
if (!$this->config instanceof Zend_Config) {
$this->initConfig();
}
return $this->config;
}
/**
* #return Application
*/
public static function getInstance()
{
if (self::$application === null) {
self::$application = new Application();
}
return self::$application;
}
/**
* Load Configuration
*/
private function initConfig()
{
$configFile = $this->appDir . '/config/application.xml';
if (!is_readable($configFile)) {
throw new Application_Exception('Config file "' . $configFile . '" is not readable');
}
$config = new Zend_Config_Xml($configFile, 'test');
$this->config = $config;
}
// snip
/**
* #param string $appDir
*/
public function init($appDir)
{
$this->appDir = $appDir;
$this->initConfig();
// snip
}
public function run ($appDir)
{
$this->init($appDir);
$front = $this->initController();
$front->dispatch();
}
}
Your bootstrap would look like this :
require 'Application.php';
try {
Application::getInstance()->run(dirname(dirname(__FILE__)));
} catch (Exception $e) {
header("HTTP/1.x 500 Internal Server Error");
trigger_error('Application Error : '.$e->getMessage(), E_USER_ERROR);
}
When you want to access the configuration you would use the following :
$var = Application::getInstance()->getConfig()->somevar;
In most ZF apps, the application object is declared in the global scope (see public/index.php in apps created with ZFW_DISTRIBUTION/bin/zf.sh).
It's not exactly the ZF way, but you can access the object with $GLOBALS['application'].
It kinda feels like cheating, but if you're after performance, this will likely be the quickest option.
$manager = new My_Model_Manager($GLOBALS['application']->getOption('my'));
$this->getInvokeArg('bootstrap')->getOptions();
// or
$configDb = $this->getInvokeArg('bootstrap')->getOption('db');
I've define a short hand in some place I require_once() in the beginning of boostrap:
function reg($name, $value=null) {
(null===$value) || Zend_Registry::set($name, $value);
return Zend_Registry::get($name);
}
and in the bootstrap I have a:
protected function _initFinal()
{
reg('::app', $this->getApplication());
}
then I can get the Application instance anywhere by use:
$app = reg('::app');
A really simple way to access the configuration options is by directly accessing the globally defined $application variable.
class My_UserController extends Zend_Controller_Action {
public function indexAction() {
global $application;
$options = $application->getOptions();
}
}