YiiBase::import() support for multiple classes in one file

It would be very useful to be able to package many classes into a single file and still use Yii's import declarations. This would also be in keeping with Yii's ability to dramatically increase performance using APC and the stated purpose of yiilite.

I have been hacking on it and have a potential solution, if you think it would be beneficial. I'll use the tired example of the class nobody has ever really built: Animal!

external class file (apppath/something/Animal.php)



<?php


class Animal {


//snip


}


class Duck extends Animal {


//snip


}


class MutantDuck extends Duck implements Mutant {


//snip


}


?>


controller:



<?php


class AwesomeController extends CController


{


    function actionDoWonderfulThings()


    {


        if (isset($_REQUEST['inHell']))


            $ohDear = new MutantDuck();


        else


            $sweet = new WonderfulThing();


    }


}


?>


If I put the appropriate import declarations at the top of the controller, I currently have to specify the force include parameter:



<?php Yii::import("application.something.Animal", true); ?>


in order to expect that the MutantDuck will be available in the event that $_REQUEST[‘inHell’] is set. That means I’m loading a whole bunch of classes I might not necessarily need - if we’re not in hell, I can’t imagine there would be too many mutant ducks! ;)

The idea I had to solve the problem is to use a different character to distinguish between a path (which we're using '.' for), and a class name which is the child of a file (for which i thought we could use '/').

The following specifies that the class MutantDuck, present in the file Animal, should be imported:



<?php Yii::import("application.something.Animal/MutantDuck"); ?>


The disadvantage is that we need to have an import declaration for each child class we want to be autoloadable… for example, if the controller needed to instantiate a regular Duck under certain circumstances, we would need to import both:



<?php


Yii::import("application.something.Animal/Duck");


Yii::import("application.something.Animal/MutantDuck");


?>


The following changes can be made to YiiBase::import() and YiiBase::getPathOfAlias() to support it (but i'm sure there will be a better way to do it). The changes are not huge, but I have not thoroughly tested them yet:



<?php


	public static function import($alias,$forceInclude=false)


	{


		if(isset(self::$_imports[$alias]))  // previously imported


			return self::$_imports[$alias];





		$classPos=strrpos($alias,'.');


		$childPos=strrpos($alias,'/');


		


		if(isset(self::$_coreClasses[$alias]) || ($classPos===false && $childPos===false))  // a simple class name


		{


			self::$_imports[$alias]=$alias;


			if($forceInclude && !class_exists($alias,false))


			{


				if(isset(self::$_coreClasses[$alias])) // a core class


					require_once(YII_PATH.self::$_coreClasses[$alias]);


				else


					require_once($alias.'.php');


			}


			return $alias;


		}





		if(($className=(string)substr($alias,max($classPos,$childPos)+1))!=='*' && class_exists($className,false))


			return self::$_imports[$alias]=$className;





		if(($path=self::getPathOfAlias($alias))!==false)


		{


			if($className!=='*')


			{


				self::$_imports[$alias]=$className;


				if($forceInclude)


					require_once($path.'.php');


				else


					self::$_classes[$className]=$path.'.php';


				return $className;


			}


			else  // a directory


			{


				set_include_path(get_include_path().PATH_SEPARATOR.$path);


				return self::$_imports[$alias]=$path;


			}


		}


		else


			throw new CException(Yii::t('yii#Alias "{alias}" is invalid. Make sure it points to an existing directory or file.',


				array('{alias}'=>$alias)));


	}





	public static function getPathOfAlias($alias)


	{


		if(isset(self::$_aliases[$alias]))


			return self::$_aliases[$alias];


		else if(($pos=strpos($alias,'.'))!==false)


		{


			$pathAlias=$alias;


			// remove the child class portion of the alias


			if(($childPos=strrpos($alias,"/"))!==false)


				$pathAlias = substr($alias,0,$childPos);


			


			$rootAlias=substr($alias,0,$pos);


			if(isset(self::$_aliases[$rootAlias]))


				return self::$_aliases[$alias]=rtrim(self::$_aliases[$rootAlias].DIRECTORY_SEPARATOR.str_replace('.',DIRECTORY_SEPARATOR,substr($pathAlias,$pos+1)),'*'.DIRECTORY_SEPARATOR);


		}


		return self::$_aliases[$alias]=false;


	}


?>


Thank you for the explorational investigation.

My opinion is that we should keep things simple. The current rule is that each public class should occupy a separate class file while private classes may stay inside a public class file. This is a widely accepted convention (e.g. Java, Flex). It also makes code maintenance easier.

I believe your approach will increase performance when using APC. However, the result won't be as dramastic as yiilite because you don't want to pack tens of classes into a single file (like yiilite) for your project. And if so, you should just include the merged file so that APC can take care of it.