I'm trying to create a unit test for a Factory class,
but PHPUnit is returning me a fatal error on the creation of the Product.
Fatal error: Class 'JsonStorage' not found
Files structure
-\
-libs
-My
-Storage
-IStorage.php
-IStorageFactory.php
-StorageFactory.php
-JsonStorage.php
-FileStorage.php
-tests
-StorageFactoryTest.php
This is the
Creator
use \tests;
namespace My\Storage;
class StorageFactory implements IStorageFactory
{
public static function build($type)
{
$classname = "Storage";
if (is_string($type)) {
switch($type = ucfirst(strtolower(trim($type)))) {
case "Json": // Fall-through
case "File":
$classname = $type . $classname;
break;
default:
throw new \Exception("Storage type not recognized or not yet implemented: please provide a valid type");
}
if (class_exists($classname)) {
try {
return new $classname;
} catch (Exception $e) {
throw new \Exception("Cannot create Storage object: please provide a valid type");
}
} else {
throw new \Exception("Class not recognized");
}
Product of the factory
can be
namespace My\Storage;
class FileStorage implements IStorage
{
private $filepath;
private $data;
private $handle = null;
public function __construct($filepath = null)
{
if (isset($filepath)) {
$this->setPath($filepath);
}
}
//....
}
or (the case I'm testing)
namespace My\Storage;
class JsonStorage extends FileStorage
{
// .... additional implementations
}
and I have a
Testing class
use \tests;
namespace My\Form\Storage;
require_once _DIR_ . "\..\My\Form\Storage\IStorage.php";
require_once .... <all the other classes>.....
require_once
class StorageFactoryTest extends \PHPUnit_Framework_TestCase
{
public function testCanBuildJSON()
{
$s = StorageFactory::build("JSON");
$this->assertInstanceOf($s, "IStorage"); // Or better directly 'JsonStorage'?
}
}
Maybe is something wrong with the namespaces, but I cannot understand why the JsonStorage class is not found.
The solution was easy (but took me a lot..)
I checked that the class was declared with get_declared_classes().
Then I read that assertInstanceOf needs a full qualified namespace if executed inside a namespace.
So I just changed the
Creator
$classname = __NAMESPACE__."\\".$classname;
if (class_exists($classname)) {
try {
return new $classname;
} //...
and the
Test
public function testCanBuildJSON()
{
$s = StorageFactory::build("JSON");
$this->assertInstanceOf(__NAMESPACE__."\\JsonStorage", $s);
}
Hope it could help someone.
PS - Feel free to add details or suggestions for better practices
Related
I have an autoloader that is placed as a php file above all other sub directories in my project.
What it does is it loads all possible classes at once for any specific server request. After further thought I concluded I need to autoload only the required classes.
What do I need to do to avoid loading other classes not needed?
If I need to post the relevant code snippets of the class files in my subdirectories, I can.
<?php
namespace autoloader;
class autoloader
{
private $directoryName;
public function __construct($directoryName)
{
$this->directoryName = $directoryName;
}
public function autoload()
{
foreach (glob("{$this->directoryName}/*.class.php") as $filename)
{
include_once $filename;
}
foreach (glob("{$this->directoryName}/*.php") as $filename)
{
include_once $filename;
}
}
}
# nullify any existing autoloads
spl_autoload_register(null, false);
# instantiate the autoloader object
$classes = [
new autoloader('request'),
new autoloader('config'),
new autoloader('controllers'),
new autoloader('models'),
new autoloader('data')
];
# register the loader functions
foreach ($classes as $class)
spl_autoload_register(array($class, 'autoload'));
All registered autoloader functions will be called when you try to instantiate a new class or until it finally loads the class or throws an error. The way you have it now, you're registering the same autoloader function again and again for each directory, and file.
What you'd want to do is something along the lines of this.
namespace autoloader;
class autoloader
{
public function __construct()
{
spl_autoload_register([$this, 'autoload']);
}
public function autoload($classname)
{
if (! file_exists("{$classname}.class.php")) {
return;
}
include_once "{$classname}.class.php";
}
}
new autoloader();
Every autoloader function gets the class FQCN passed into it, and from there you'll have to parse it and figure out if you can load the file where that class exists. For instance, if I do the following.
use Some\Awesome\ClassFile;
$class = new ClassFile();
The autoloader we've registered will get the string Some\Awesome\ClassFile passed in as an argument, which we can then parse and see if we have a file for that class, if we don't we return out of the function and let the next registered autoloader function try and find the class.
You can read more about autoloaders in the documentation, I also wrote a blog post about it like 2 months ago that might interest you.
I had to refactor the code and remove the unnecessary load all functionality that I mistakenly thought would lazy load my classes on request.
Here is what I came up with:
Entry Point
<?php
require_once 'enums.php';
require_once 'api.class.php';
spl_autoload('AutoLoader\AutoLoader');
use App\API;
class MyAPI extends API
{
public function __construct($request){
parent::__construct($request);
}
}
$api = new MyAPI($_REQUEST);
echo $api->processRequest();
AutoLoader Implementation (located under subdirectory Autoloader/Autoloader.php and inaccessible via browser by using .htaccess)
<?php
namespace Autoloader;
spl_autoload_register("AutoLoader\AutoLoader::ClassLoader");
spl_autoload_register("AutoLoader\AutoLoader::RequestLoader");
class Autoloader
{
public static function ClassLoader(String $fileName)
{
foreach ([".Class.php", ".php"] as $extension)
if (file_exists($fileName.$extension))
include $fileName.$extension;
}
public static function RequestLoader()
{
self::ClassLoader('Request');
}
}
Snippet for processRequest() (located in api.class.php - my request router)
public function processRequest()
{
$id1 = $this->requestObj->id1;
$id2 = $this->requestObj->id2;
$endpoint1 = $this->requestObj->endpoint1;
$endpoint2 = $this->requestObj->endpoint2;
$goto = $this->requestObj->goto;
$isDestination = in_array($id1, ['first', 'prev', 'next', 'last']);
$numSetEndpoints = (int)isset($endpoint1) + (int)isset($endpoint2);
switch($numSetEndpoints)
{
case 0:
if ($isDestination)
return json_decode($this->_response("No Endpoint: ", $endpoint1));
return json_decode($this->_response("No Endpoint: " . $endpoint2 ?? $endpoint));
case 1:
$className = $endpoint1.'Controller';
break;
case 2:
$className = $endpoint2.'Controller';
break;
}
$class = "\\Controllers\\$className";
if (class_exists($class))
{
$method = strtolower($this->method);
if (method_exists($class, $method))
{
$response = (new $class($this->requestObj))->{$method}();
if ($response['Succeeded'] == false)
{
return $response['Result'];
}
else if ($response['Succeeded'] == true)
{
header("Content-Type: application/json");
return $this->_response($response);
}
else if ($response['Result'])
{
header("Content-Type: text/html");
return $this->_response($response);
}
}
}
}
I have a api trait that connects to an external endpoint. I want to use this trait in a class called ProductClass. The trait is in the same folder as the class, but I get a error is I add use ApiTrait in the class. Error says it cannot find the trait, So if I include the trait file at the top of the class file, I get this error, cannot find ApiTrait in
ProductClass\ApiTrait.
If i pass the trait into the constructor I get an error from my index page when I call the ProductClass because I am not passing in the trait. I dont want to pass any params to the constructor just the string top append to the .env endpoint. any clues greatly appreciated
heres my ApiTrait code
<?php
namespace ApiTrait;
require './vendor/autoload.php';
use GuzzleHttp\Client;
trait ApiTrait
{
protected $url;
protected $client;
public function __construct()
{
$this->url = getenv('API_URL');
$this->client = new Client();
}
private function getResponse(String $uri = null)
{
$full_path = $this->url;
$full_path .=$uri;
try {
$response = $this->client->get($full_path);
}
catch (GuzzleHttp\Exception\ClientException $e) {
$response = $e->getResponse();
}
return json_decode($response->getBody()->getContents(), true);
}
public function getAPIData($uri)
{
return $this->getResponse($uri);
}
}
this is my ProductClass code
<?php
namespace ProductClass;
include_once("ApiTrait.php");
use DataInterface\DataInterface;
class Product implements DataInterface
{
use ApiTrait\ApiTrait
private $api;
public function __construct(ApiTrait\ApiTrait $apiTrait) {
$this->api = $apiTrait;
}
private function getResponse($append, $try) {
$urlAppend = $append;
$good_data = false;
do{
try{
$result = $this->api->getAPIData($urlAppend);
//check data to see if valid
if(!array_key_exists( "error",$result)){
$good_data = true;
return $result;
}
}
catch(Exception $e){
//call api upto 10 times
if($try < 10) {
sleep(1);
getData($append, $try++);
} else { //return a connection error
$api_error['error']='unable to connect to api';
return $api_error;
}
}
} while($good_data === false);
}
public function getData($append, $try = 0)
{
return $this->getResponse($append, $try);
}
}
If you're using an autloader, you shouldn't ever need this:
include_once("ApiTrait.php");
You've got your trait defined in the ApiTrait namespace:
namespace ApiTrait;
trait ApiTrait { ... }
I.e., the trait's full path is \ApiTrait\ApiTrait. If you're using the trait in a namespace other than the one it's defined, then you need to anchor from the root namespace when referring to it, by preceding it with a backslash:
namespace ProductClass;
class Product implements DataInterface
{
use \ApiTrait\ApiTrait;
Otherwise, if you do use ApiTrait\ApiTrait; without the leading backslash, then PHP thinks you're referring to the current namespace, which is ProductClass, yielding \ProductClass\ApiTrait\ApiTrait -- which doesn't exist, hence your error.
You could also do it this way with class aliases:
namespace ProductClass;
use ApiTrait\ApiTrait;
class Product implements DataInterface
{
use ApiTrait;
Also, it looks like you're just putting every class it its own namespace. Don't do that. Use namespaces to group common items, for example, something like this:
namespace Traits;
trait Api { ... }
namespace Traits;
trait Foo { ... }
namespace Traits;
trait Bar { ... }
namespace App;
class Product {
use \Traits\Api;
use \Traits\Foo;
use \Traits\Bar;
}
I want to create an object of a class from a returned string but I am getting error Class **test_report** not found. My code:
public function display_report_builder($report_name = null)
{
$column_listing = new $report_name;// gets the test_report
return view('column_list')->with(['column_list_names' => $column_listing->columns]);
}
This isn't the better approach here. What you should do is to use a Factory design pattern:
class ReportFactory
{
public static function create($report_name)
{
switch($report_name) {
case 'test_report': return new TestReport();
default: throw new Exception('report not found');
}
}
}
Then you call with $column_listing = ReportFactory::create($report_name);
Why? Because you avoid "magic variables" with unknown data; you can trace errors properly; you can use namespace; you can extend functionalities easily, and easily activate or deactivate objects (or reports in this case); you have a cleaner code, and so on...
test if the class name (string) really is a valid class :
public function display_report_builder($report_name = null)
{
$column_list_names = null;
if (class_exists($report_name) && is_a($report_name, App\reports\test_report::class, true)) {
$column_listing = new $report_name;
$column_list_names = $column_listing->columns;
}
return view('column_list', compact('column_list_names'));
}
is_a() : Checks if the given object is of this class or has this class
as one of its parents.
I wrote an Autoload:
<?php
function autoload($class)
{
try
{
global $path;
$elements = explode("\\", $class);
$name_class = end($elements);
foreach ($elements as $element)
{
if($element != $name_class)
{
$path .= $element."/";
echo "path:".$path."<br/>";
}
}
$file = strtolower(($path).$name_class.".php");
if(file_exists($file))
{
include($file);
return;
}
throw new Exception($class." not founded");
}
catch(Exception $e)
{
echo $e->getMessage();
return;
}
}
class Autoloader
{
public static function autoload($class)
{
autoload($class);
}
}
spl_autoload_register('autoload');
spl_autoload_register(array('autoloader', 'autoload'));
?>
The Autoload finds a relative path and change it in an absolute path.
Now I have problem when I call a class from another file with different namespace.
For example if I have this file Base.php in the directorynamed framework:
namespace Framework;
use Framework\Inspector as Inspector;
class Base
{
private $_inspector;
public function __construct($options = array())
{
$this->_inspector = new Inspector($this);
...
Now I have the file Inspector.php in the same directory with the same namespace:
<?php
namespace Framework;
use Framework\ArrayMethods as ArrayMethods;
use Framework\StringMethods as StringMethods;
use \Exception as Exception;
class Inspector
{
...
?>
When I try to instantiate the class Base, I get the exception message about missing Inspector class.
The same think if I try to extend Base class in other directories. I tried to add some echoes in my autoload function and it printed that he use local namespace of the child class or a class who uses an instance by composition of another class to create the string for finding the php file.
For example when I try the load Inspector in Base with echo I got:
path:Framework/
file:::C:\LightTPD\htdocs\mvc\framework\framework\Base.php (I add some ::: to make more readable)
framework/base.php
path:Framework/Framework/
file:::
framework/framework/inspector.php (trying to use the namespace of inspector after using the one of Base)
Framework\Inspector not foundedpath:Framework/Framework/Framework/
file:::
framework/framework/framework/inspector.php
Framework\Inspector not founded
When I tried to extend class Base by class Driver I got this:
path:Framework/
path:Framework/Configuration/
file:::C:\LightTPD\htdocs\mvc\framework\framework\configuration\Driver.php
framework/configuration/driver.php (everything ok)
path:Framework/Configuration/Framework/ (Framework is from Base; Configuration/Framework is from driver)
file:::
framework/configuration/framework/base.php
Framework\Base not foundedpath:Framework/Configuration/Framework/Framework/
file:::
framework/configuration/framework/framework/base.php
Framework\Base not founded
-###############################################################
The directory tree is:
It's:
-framework: ArrayMethods.php, Base.php,Configuration.php, Inspector.php, StingMethods.php
-configuration: Driver.php
-exception
-core
-exception
How can I fix it?
Sorry I noticed that the problem was the global $path.
Now I wrote the autoload.php in this way:
<?php
function autoload($class)
{
try
{ $path = NULL;
$elements = explode("\\", $class);
$name_class = end($elements);
foreach ($elements as $element)
{
if($element != $name_class)
{
$path .= $element."/";
$file = realpath(strtolower(($path).$name_class.".php")); //realpath is optional
if(file_exists($file))
{
include($file);
//echo $file."<br><br>"; testing in for debugging
return;
}
}
}
throw new Exception($class." not founded");
}
catch(Exception $e)
{
echo $e->getMessage();
return;
}
}
class Autoloader
{
public static function autoload($class)
{
autoload($class);
}
}
spl_autoload_register('autoload');
spl_autoload_register(array('autoloader', 'autoload'));
?>
Now even if it's less perfomant everything it's ok, he work whatever a class call another, it's based only the namespace of a class.
Make sure you include the files path of the class you want to extend(parent class) from to your child class file before the use namespaces of the parent/Base class.
Is there a way to throw exceptions from an SPL Autoloader in PHP in case it fails? It doesn't seem to work under PHP 5.2.11.
class SPLAutoLoader{
public static function autoloadDomain($className) {
if(file_exists('test/'.$className.'.class.php')){
require_once('test/'.$className.'.class.php');
return true;
}
throw new Exception('File not found');
}
} //end class
//start
spl_autoload_register( array('SPLAutoLoader', 'autoloadDomain') );
try{
$domain = new foobarDomain();
}catch(Exception $c){
echo 'File not found';
}
When the above code is called, there is no sign of an exception, instead I get a standard "Fatal error: Class 'foobarDomain' not found in bla". And the execution of the script terminates.
This is not a bug, it's a design decision:
Note: Exceptions thrown in __autoload function cannot be caught in the catch block and results in a fatal error.
The reason is that there may be more than one autoload handlers, in which case, you don't want the first handler to throw an Exception and bypass the second handler. You want your second handler to have a chance at autoloading its classes. If you use a library which makes use of the autoloading feature, you don't want it bypassing your autoload handler because they throw Exceptions inside their autoloader.
If you want to check whether or not you can instantiate a class, then use class_exists and pass true as the second argument (or leave it out, true is the default):
if (class_exists('foobarDomain', $autoload = true)) {
$domain = new foobarDomain();
} else {
echo 'Class not found';
}
According to the comments in the documentation for spl_autoload_register, it's possible to call another function from the autoloader, which in turn would throw the exception.
class SPLAutoLoader{
public static function autoloadDomain($className) {
if(file_exists('test/'.$className.'.class.php')){
require_once('test/'.$className.'.class.php');
return true;
}
self::throwFileNotFoundException();
}
public static function throwFileNotFoundException()
{
throw new Exception('File not found');
}
} //end class
//start
spl_autoload_register( array('SPLAutoLoader', 'autoloadDomain') );
try{
$domain = new foobarDomain();
}catch(Exception $c){
echo 'File not found';
}
Here's a full-fledged factory object which demonstrates auto-loading, namespaces support, callables from non-static instances (with variable paths), handling of loading errors and custom exceptions.
abstract class AbstractFactory implements \ArrayAccess
{
protected $manifest;
function __construct($manifest)
{
$this->manifest = $manifest;
}
abstract function produce($name);
public function offsetExists($offset)
{
return isset($this->manifest[$offset]);
}
public function offsetGet($offset)
{
return $this->produce($offset);
}
//implement stubs for other ArrayAccess funcs
}
abstract class SimpleFactory extends AbstractFactory {
protected $description;
protected $path;
protected $namespace;
function __construct($manifest, $path, $namespace = "jj\\") {
parent::__construct($manifest);
$this->path = $path;
$this->namespace = $namespace;
if (! spl_autoload_register(array($this, 'autoload'), false)) //throws exceptions on its own, but we want a custom one
throw new \RuntimeException(get_class($this)." failed to register autoload.");
}
function __destruct()
{
spl_autoload_unregister(array($this, 'autoload'));
}
public function autoload($class_name) {
$file = str_replace($this->namespace, '', $class_name);
$filename = $this->path.$file.'.php';
if (file_exists($filename))
try {
require $filename; //TODO add global set_error_handler and try clause to catch parse errors
} catch (Exception $e) {} //autoload exceptions are not passed by design, nothing to do
}
function produce($name) {
if (isset($this->manifest[$name])) {
$class = $this->namespace.$this->manifest[$name];
if (class_exists($class, $autoload = true)) {
return new $class();
} else throw new \jj\SystemConfigurationException('Factory '.get_class($this)." was unable to produce a new class {$class}", 'SYSTEM_ERROR', $this);
//an example of a custom exception with a string code and data container
} else throw new LogicException("Unknown {$this->description} {$name}.");
}
function __toString() //description function if custom exception class wants a string explanation for its container
{
return $this->description." factory ".get_class($this)."(path={$this->path}, namespace={$this->namespace}, map: ".json_encode($this->manifest).")";
}
}
and finally an example:
namespace jj;
require_once('lib/AbstractFactory.php');
require_once('lib/CurrenciesProvider.php'); //base abstract class for all banking objects that are created
class CurrencyProviders extends SimpleFactory
{
function __construct()
{
$manifest = array(
'Germany' => 'GermanBankCurrencies',
'Switzerland' => 'SwissBankCurrencies'
);
parent::__construct($manifest, __DIR__.'/CurrencyProviders/', //you have total control over relative or absolute paths here
'banks\');
$this->description = 'currency provider country name';
}
}
now do
$currencies_cache = (new \jj\CurrencyProviders())['Germany'];
or
$currencies_cache = (new \jj\CurrencyProviders())['Ukraine'];
LogicException("Unknown currency provider country name Ukraine")
If there is no SwissCurrencies.php file in /CurrencyProviders/,
\jj\SystemConfigurationException('Factory jj\CurrencyProviders was unable to produce a new class banks\SwissCurrencies. Debug data: currency provider country name factory jj\CurrencyProviders(path=/var/www/hosted/site/.../CurrencyProviders/, namespace=banks\, map: {"Germany": "GermanBankCurrencies", "Switzerland":"SwissBankCurrencies"}')
With enough effort this factory can be extended to catch parse errors (How to catch error of require() or include() in PHP?) and pass arguments to constructors.