Emmanuel BENOîT
47b759d993
Added a method to the loader which can find all sub-classes / implementations of a class / interface. The method must be called with at least one argument (the name of the class / interface to find subclasses / implementations of). By default Loader::Find() will look for 'extra' classes. However, it is possible to specify something else (e.g. 'ctrl' for controllers) as the second argument. Finally, by default, only packages which have already been loaded when the method is called are considered. Passing false as the 3rd argument will cause packages to be loaded.
537 lines
11 KiB
PHP
537 lines
11 KiB
PHP
<?php
|
|
|
|
|
|
final class LoaderException extends Exception { }
|
|
|
|
|
|
final class ConfigGetter
|
|
{
|
|
private $config;
|
|
private $package;
|
|
|
|
public function __construct( Package $package , $config )
|
|
{
|
|
$this->package = $package;
|
|
$this->config = $config;
|
|
}
|
|
|
|
|
|
public function get( $path = '' , $default = null , $fail = false )
|
|
{
|
|
if ( $path == '' ) {
|
|
return $this->config;
|
|
}
|
|
|
|
$aPath = explode( '/' , $path );
|
|
$config = &$this->config;
|
|
foreach ( $aPath as $name ) {
|
|
if ( !( is_array( $config ) && array_key_exists( $name , $config ) ) ) {
|
|
if ( $fail ) {
|
|
throw new LoaderException( "configuration key '$path' not found for package '"
|
|
. $this->package->name() . "'" );
|
|
}
|
|
return $default;
|
|
}
|
|
$config = &$config[ $name ];
|
|
}
|
|
return $config;
|
|
}
|
|
}
|
|
|
|
|
|
final class Package
|
|
{
|
|
private $name;
|
|
private $source;
|
|
|
|
private $files;
|
|
private $requires;
|
|
|
|
private $ctrls;
|
|
private $daos;
|
|
private $extras;
|
|
private $hooks;
|
|
private $pages;
|
|
private $singletons;
|
|
private $views;
|
|
|
|
private $loaded = false;
|
|
private $config;
|
|
|
|
public function __construct( $name , $description , $config , $source )
|
|
{
|
|
$this->name = $name;
|
|
$this->source = $source;
|
|
|
|
$fields = array( 'files' , 'requires' , 'daos' , 'views' , 'ctrls' , 'extras' , 'singletons' , 'pages' , 'hooks' );
|
|
foreach ( $fields as $field ) {
|
|
$this->getField( $description , $field );
|
|
}
|
|
if ( empty( $this->files ) ) {
|
|
throw new LoaderException( "package '{$this->name}': no files" );
|
|
}
|
|
|
|
if ( ! is_array( $config ) ) {
|
|
$config = array( );
|
|
}
|
|
$this->config = new ConfigGetter( $this , $config );
|
|
}
|
|
|
|
|
|
private function getField( $description , $field )
|
|
{
|
|
if ( ! array_key_exists( $field , $description ) ) {
|
|
$this->$field = array( );
|
|
return;
|
|
}
|
|
|
|
$value = $description[ $field ];
|
|
if ( !is_array( $value ) ) {
|
|
throw new LoaderException( "package '{$this->name}': '$field' must be an array" );
|
|
}
|
|
|
|
foreach ( $value as $item ) {
|
|
if ( !is_string( $item ) ) {
|
|
throw new LoaderException( "package '{$this->name}': '$field' contains non-string items" );
|
|
}
|
|
}
|
|
|
|
$this->$field = $value;
|
|
}
|
|
|
|
|
|
public function name( )
|
|
{
|
|
return $this->name;
|
|
}
|
|
|
|
public function source( )
|
|
{
|
|
return $this->source;
|
|
}
|
|
|
|
public function files( )
|
|
{
|
|
return $this->files;
|
|
}
|
|
|
|
public function requires( )
|
|
{
|
|
return $this->requires;
|
|
}
|
|
|
|
public function daos( )
|
|
{
|
|
return $this->daos;
|
|
}
|
|
|
|
public function views( )
|
|
{
|
|
return $this->views;
|
|
}
|
|
|
|
public function ctrls( )
|
|
{
|
|
return $this->ctrls;
|
|
}
|
|
|
|
public function singletons( )
|
|
{
|
|
return $this->singletons;
|
|
}
|
|
|
|
public function extras( )
|
|
{
|
|
return $this->extras;
|
|
}
|
|
|
|
public function pages( )
|
|
{
|
|
return $this->pages;
|
|
}
|
|
|
|
|
|
public function loaded( )
|
|
{
|
|
return $this->loaded;
|
|
}
|
|
|
|
public function hooks( )
|
|
{
|
|
return $this->hooks;
|
|
}
|
|
|
|
public function setLoaded( )
|
|
{
|
|
$this->loaded = true;
|
|
}
|
|
|
|
|
|
public function config( $path = '' , $default = null , $fail = false )
|
|
{
|
|
return $this->config->get( $path , $default , $fail );
|
|
}
|
|
|
|
public function getConfigAccess( )
|
|
{
|
|
return $this->config;
|
|
}
|
|
}
|
|
|
|
|
|
interface PackageAware
|
|
{
|
|
public function setPackage( Package $package );
|
|
}
|
|
|
|
|
|
interface TextSource
|
|
{
|
|
public function get( $what );
|
|
}
|
|
|
|
|
|
final class Loader
|
|
{
|
|
private static $loader = null;
|
|
|
|
private static $paths = array();
|
|
private $config;
|
|
private $packages = array( );
|
|
private $items = array(
|
|
'ctrls' => array( ) ,
|
|
'daos' => array( ) ,
|
|
'extras' => array( ) ,
|
|
'singletons' => array( ) ,
|
|
'views' => array( ) ,
|
|
'pages' => array( ) ,
|
|
);
|
|
private $loading = array( );
|
|
private $singletons = array( );
|
|
private $daos = array( );
|
|
private $textSource;
|
|
|
|
private function __construct( )
|
|
{
|
|
if ( empty( Loader::$paths ) ) {
|
|
array_push( Loader::$paths , dirname( __FILE__ ) );
|
|
}
|
|
$this->loadConfig( );
|
|
$this->loadPackageDescriptions( );
|
|
}
|
|
|
|
private function loadConfig( )
|
|
{
|
|
$mergedConfig = array( );
|
|
foreach ( Loader::$paths as $directory ) {
|
|
if ( ! file_exists( $directory . '/config.inc.php' ) ) {
|
|
continue;
|
|
}
|
|
|
|
$config = array( );
|
|
@include( $directory . '/config.inc.php' );
|
|
$mergedConfig = array_merge_recursive( $mergedConfig , $config );
|
|
}
|
|
$this->config = $mergedConfig;
|
|
}
|
|
|
|
private function loadPackageDescriptions( )
|
|
{
|
|
foreach ( Loader::$paths as $source ) {
|
|
$this->loadPackageDescriptionsFrom( $source );
|
|
}
|
|
}
|
|
|
|
private function loadPackageDescriptionsFrom( $source )
|
|
{
|
|
if ( !( $dh = opendir( $source ) ) ) {
|
|
throw new LoaderException( "unable to access directory" );
|
|
}
|
|
|
|
while ( ( $entry = readdir( $dh ) ) !== false ) {
|
|
if ( $entry === '.' || $entry === '..' ) {
|
|
continue;
|
|
}
|
|
|
|
$path = "$source/$entry";
|
|
if ( is_dir( $path ) && is_file( "$path/package.inc.php" ) ) {
|
|
$this->loadDescription( $entry , $source );
|
|
}
|
|
}
|
|
|
|
closedir( $dh );
|
|
}
|
|
|
|
|
|
private function loadDescription( $name , $source )
|
|
{
|
|
$package = array( );
|
|
require( $source . '/' . $name . '/package.inc.php' );
|
|
if ( empty( $package ) ) {
|
|
throw new LoaderException( "package '$name': no information" );
|
|
}
|
|
|
|
if ( ! array_key_exists( $name , $this->config ) ) {
|
|
$this->config[ $name ] = array( );
|
|
}
|
|
|
|
$package = new Package( $name , $package , $this->config[ $name ] , $source );
|
|
$this->packages[ $name ] = $package;
|
|
$this->config[ $name ] = null;
|
|
|
|
foreach ( array_keys( $this->items ) as $type ) {
|
|
$items = $package->$type( );
|
|
foreach ( $items as $item ) {
|
|
if ( array_key_exists( $item , $this->items[ $type ] ) ) {
|
|
$oName = $this->items[ $type ][ $item ];
|
|
$type = substr( $type , 0 , strlen( $type ) - 1 );
|
|
throw new LoaderException( "package '$name': conflict with '$oName' on $type '$item'" );
|
|
}
|
|
$this->items[ $type ][ $item ] = $name;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private function loadPackage( $name )
|
|
{
|
|
if ( ! array_key_exists( $name , $this->packages ) ) {
|
|
throw new LoaderException( "Package '$name' not found" );
|
|
}
|
|
|
|
$package = $this->packages[ $name ];
|
|
if ( $package->loaded( ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( array_key_exists( $name , $this->loading ) ) {
|
|
throw new LoaderException( "Package '$name': recursive dependencies detected" );
|
|
}
|
|
$this->loading[ $name ] = 1;
|
|
|
|
foreach ( $package->requires( ) as $dependency ) {
|
|
$this->loadPackage( $dependency );
|
|
}
|
|
|
|
$dir = $package->source() . '/' . $name;
|
|
foreach ( $package->files( ) as $file ) {
|
|
require_once( "$dir/$file.inc.php" );
|
|
}
|
|
|
|
unset( $this->loading[ $name ] );
|
|
$package->setLoaded( );
|
|
|
|
$hooks = $package->hooks( );
|
|
if ( is_array( $hooks ) ) {
|
|
foreach ( $hooks as $hook ) {
|
|
$hook( $this , $package );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private function findItem( $name , $type )
|
|
{
|
|
$rType = $type . 's';
|
|
if ( ! array_key_exists( $rType , $this->items ) ) {
|
|
throw new LoaderException( "Invalid type '$type'" );
|
|
}
|
|
|
|
if ( ! array_key_exists( $name , $this->items[ $rType ] ) ) {
|
|
throw new LoaderException( "Item '$name' of type $type not found" );
|
|
}
|
|
|
|
$package = $this->items[ $rType ][ $name ];
|
|
$this->loadPackage( $package );
|
|
return $this->packages[ $package ];
|
|
}
|
|
|
|
|
|
private function createInstance( $package , $cName , $args )
|
|
{
|
|
if ( empty( $args ) ) {
|
|
$instance = new $cName();
|
|
} else {
|
|
try {
|
|
$reflection = new ReflectionClass( $cName );
|
|
} catch ( ReflectionException $e ) {
|
|
throw new LoaderException( "Class $cName from package $pName not found" );
|
|
}
|
|
$instance = $reflection->newInstanceArgs( $args );
|
|
}
|
|
|
|
if ( is_a( $instance , 'PackageAware' ) ) {
|
|
$instance->setPackage( $package );
|
|
}
|
|
return $instance;
|
|
}
|
|
|
|
|
|
private function loadAndCreate( $type , $name , $cName , $args )
|
|
{
|
|
$package = $this->findItem( $name , $type );
|
|
return $this->createInstance( $package , $cName , $args );
|
|
}
|
|
|
|
|
|
private function getSingleton( $name )
|
|
{
|
|
if ( ! array_key_exists( $name , $this->singletons ) ) {
|
|
$this->singletons[ $name ] = $this->loadAndCreate( 'singleton' , $name , $name , array( ) );
|
|
}
|
|
return $this->singletons[ $name ];
|
|
}
|
|
|
|
|
|
private function getDao( $name )
|
|
{
|
|
if ( array_key_exists( $name , $this->daos ) ) {
|
|
return $this->daos[ $name ];
|
|
}
|
|
$cName = Loader::convertName( 'DAO' , $name );
|
|
$instance = $this->loadAndCreate( 'dao' , $name , $cName , array( ) );
|
|
$instance->setDatabase( Loader::Singleton( 'Database' ) );
|
|
$this->daos[ $name ] = $instance;
|
|
return $instance;
|
|
}
|
|
|
|
public function findClasses( $class , $type = 'extra' , $onlyLoadedPackages = true )
|
|
{
|
|
$rType = $type . 's';
|
|
if ( ! array_key_exists( $rType , $this->items ) ) {
|
|
throw new LoaderException( "Invalid type '$type'" );
|
|
}
|
|
$convertName = ( $type !== 'extra' );
|
|
|
|
$result = array( );
|
|
foreach ( $this->items[ $rType ] as $item => $pName ) {
|
|
$cName = $convertName ? Loader::convertName( $type , $item ) : $item;
|
|
if ( ! strcasecmp( $cName , $class ) ) {
|
|
continue;
|
|
}
|
|
|
|
$package = $this->packages[ $pName ];
|
|
if ( ! $package->loaded( ) ) {
|
|
if ( $onlyLoadedPackages ) {
|
|
continue;
|
|
}
|
|
$this->loadPackage( $pName );
|
|
}
|
|
try {
|
|
$ref = new ReflectionClass( $cName );
|
|
} catch ( ReflectionException $e ) {
|
|
throw new LoaderException( "Class $cName for $item of type $type and package $pName not found" );
|
|
}
|
|
|
|
if ( $ref->implementsInterface( $class ) || $ref->isSubclassOf( $class ) ) {
|
|
$result[] = $item;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
private static function get( )
|
|
{
|
|
if ( Loader::$loader === null ) {
|
|
Loader::$loader = new Loader( );
|
|
}
|
|
return Loader::$loader;
|
|
}
|
|
|
|
private static function convertName( $type , $name )
|
|
{
|
|
$cName = ucfirst( $type ) . '_';
|
|
foreach ( explode( '_' , $name ) as $part ) {
|
|
$cName .= ucfirst( $part );
|
|
}
|
|
return $cName;
|
|
}
|
|
|
|
|
|
public static function DirectCreate( $type , $convert , $args )
|
|
{
|
|
$name = array_shift( $args );
|
|
$cName = $convert ? Loader::convertName( $type , $name ) : $name;
|
|
return Loader::get( )->loadAndCreate( $type , $name , $cName , $args );
|
|
}
|
|
|
|
|
|
public static function AddPath( $path )
|
|
{
|
|
if ( empty( Loader::$paths ) ) {
|
|
array_push( Loader::$paths , dirname( __FILE__ ) );
|
|
}
|
|
if ( is_dir( $path ) ) {
|
|
array_push( Loader::$paths , $path );
|
|
}
|
|
}
|
|
|
|
|
|
public static function PackageConfig( $name )
|
|
{
|
|
$loader = Loader::get( );
|
|
$loader->loadPackage( $name );
|
|
return $loader->packages[ $name ]->getConfigAccess( );
|
|
}
|
|
|
|
|
|
public static function TextSource( TextSource $source = null )
|
|
{
|
|
if ( $source !== null ) {
|
|
Loader::get( )->textSource = $source;
|
|
} else {
|
|
return Loader::get( )->textSource;
|
|
}
|
|
}
|
|
|
|
|
|
public static function Text( $what )
|
|
{
|
|
$source = Loader::get( )->textSource;
|
|
return $source ? $source->get( $what ) : $what;
|
|
}
|
|
|
|
|
|
public static function Load( $name , $type = 'extra' )
|
|
{
|
|
Loader::get( )->findItem( $name , $type );
|
|
}
|
|
|
|
|
|
public static function Singleton( $name )
|
|
{
|
|
return Loader::get( )->getSingleton( $name );
|
|
}
|
|
|
|
public static function Create( )
|
|
{
|
|
return Loader::DirectCreate( 'extra' , false , func_get_args( ) );
|
|
}
|
|
|
|
public static function View( )
|
|
{
|
|
return Loader::DirectCreate( 'view' , true , func_get_args( ) );
|
|
}
|
|
|
|
public static function Ctrl( )
|
|
{
|
|
return Loader::DirectCreate( 'ctrl' , true , func_get_args( ) );
|
|
}
|
|
|
|
public static function Page( )
|
|
{
|
|
return Loader::DirectCreate( 'page' , true , func_get_args( ) );
|
|
}
|
|
|
|
public static function DAO( $name )
|
|
{
|
|
return Loader::get( )->getDao( $name );
|
|
}
|
|
|
|
public static function Find( $class , $type = 'extra' , $onlyLoadedPackages = true )
|
|
{
|
|
return Loader::get( )->findClasses( $class , $type , $onlyLoadedPackages );
|
|
}
|
|
}
|