Posted: November 22, 2011 in Web

Over the years of working with the Symfony PHP framework I have had moments of conflict about whether I made the right choice to use the Propel ORM. Both Propel and Doctrine have amazing features but ultimately I like the Propel style. Development on the project waxes and wanes but as of late it has been moving at a rapid pace. One feature I have always liked about Doctrine that Propel does not have is pruning classes. For example if I have a schema file and decide to change the name of a table or delete a table altogether, all of my model, form and filter classes from the previous table remain. Since there are many folders in which these auto-generated classes reside it’s a huge pain to go through them all and remove them. Today I wrote a quick little Symfony task to take care of this for me and I thought I’d share it with the rest of the Symfony+Propel world. Since I needed this quickly it’s a little dirty and only works with a filename named “schema.xml” so if you need it to work with multiple XML schemas you will have to tweak the code a little. I didn’t bother with YML schema files because I don’t use them, enjoy!

The code is listed below or you may clone a copy from github.

Save the code below in a file lib/task/PropelPruneClassesTask.class.php

Usage: ./symfony propel:prune-classes –exclude=”MyCustomFormClass.class.php MyCustomFileInModel.php”

The –exclude flag is optional. If you have any custom classes of your own that reside in your lib/model, lib/filter or lib/form folders you include a space-delimited list of them in the exclude option.


< ?php
class PropelPruneClassesTask extends sfBaseTask
{

	private $prefixes;
	private $postfixes;
	private $tableNames;
	private $exclude;
	private $toDelete;

	public function configure() {
		$this->namespace = 'propel';
		$this->name      = 'prune-classes';
		$this->briefDescription = 'Removes unused model, filter and form classes';
		$this->addOptions(array(
      new sfCommandOption('application', null, sfCommandOption::PARAMETER_REQUIRED, 'The application name', 'frontend'),
	    new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'prod'),
	    new sfCommandOption('exclude', null, sfCommandOption::PARAMETER_OPTIONAL, 'A space delimited list of files to exclude from removal', "BaseForm.class.php BaseFormPropel.class.php BaseFormFilterPropel.class.php"),
		));
	}
	
	public function execute($arguments = array(), $options = array()) {
		$this->toDelete = array();
		$deleteCount = 0;
		$this->exclude = array("BaseForm.class.php", "BaseFormPropel.class.php", "BaseFormFilterPropel.class.php");
		if (!is_null($options['exclude']))
			$this->exclude = array_merge($this->exclude, explode(' ', $options['exclude']));
		
		$schemaXmlFile = sfConfig::get('sf_config_dir')."/schema.xml";
		$fp = fopen($schemaXmlFile, "r");
		$contents = fread($fp, filesize($schemaXmlFile));
		$xml = new SimpleXMLElement($contents);
		foreach ($xml->table as $table) {
			$rawTableName = $table['name'];
			$this->tableNames[] = str_replace(' ', '', ucwords(str_replace('_', ' ', $rawTableName)));
		}
		$libDir = sfConfig::get('sf_lib_dir');
		$modelDir = $libDir.'/model';
		$baseModelDir = $modelDir."/om";
		$mapDir = $modelDir."/map";
		$formDir = $libDir.'/form';
		$baseFormDir = $formDir.'/base';
		$filterDir = $libDir.'/filter';
		$baseFilterDir = $filterDir.'/base';
	
		$this->prefixes = array();
		$this->postfixes = array('Peer', 'Query');
		$this->buildFileRemovalList($modelDir);
	
		$this->prefixes = array('Base');
		$this->postfixes = array('Peer', 'Query');
		$this->buildFileRemovalList($baseModelDir);
	
		$this->prefixes = array();
		$this->postfixes = array('TableMap');
		$this->buildFileRemovalList($mapDir);
	
		$this->postfixes = array('Form.class');
		$this->buildFileRemovalList($formDir);
	
		$this->prefixes = array('Base');
		$this->postfixes = array('Form.class');
		$this->buildFileRemovalList($baseFormDir);
	
		$this->prefixes = array();
		$this->postfixes = array('FormFilter.class');
		$this->buildFileRemovalList($filterDir);
	
		$this->prefixes = array('Base');
		$this->postfixes = array('FormFilter.class');
		$this->buildFileRemovalList($baseFilterDir);
	
		if (count($this->toDelete) > 0) {
			$doDelete = $this->askConfirmation("The following files are scheduled for deletion: \n\n".implode("\n", $this->toDelete)."\n\n Perform deletion? (y/n)");
			if ($doDelete) {
				foreach ($this->toDelete as $d) {
					if (unlink($d)) {
						$this->logSection('file-', $d);
						$deleteCount++;
					}
					else
						$deleteError[] = $d;
				}
				if (count($deleteError) > 0)
					$this->logBlock("The following files could not be removed: \n\n".implode("\n", $deleteError), 'ERROR_LARGE');
			}
		}
	
		if ($deleteCount == 1)
			$this->logBlock($deleteCount." file pruned", 'INFO_LARGE');
		else
			$this->logBlock($deleteCount." files pruned", 'INFO_LARGE');
	}


	private function buildFileRemovalList($dirPath) {
	
		$deleteError = array();
		$dirHandle = opendir($dirPath);
		$masterFileList = $this->tableNames;
		foreach ($this->prefixes as $prefix) {
			foreach ($masterFileList as $tableName)
				$masterFileList[] = $prefix.$tableName;
		}
		foreach ($this->postfixes as $postfix) {
			foreach ($masterFileList as $tableName)
				$masterFileList[] = $tableName.$postfix;
		}
		
		$appendPhpExt = function($filename) {
			return $filename.".php";
		};
		
		$masterFileList = array_map($appendPhpExt, $masterFileList);
		
		while (false !== ($file = readdir($dirHandle))) {
			if ($file[0] != '.' && !is_dir($dirPath."/".$file) && !in_array($file, $this->exclude)) {
				if (!in_array($file, $masterFileList))
					$this->toDelete[] = $dirPath."/".$file;
			}
		}
		closedir($dirHandle);
	}

}
?>

I graduated from LSU with a B.F.A. in Graphic Design and a minor in Art History. My passions are web design and front-end development with over 10 years of experience.


Leave a comment

Did this article help you? Do you have a different opinion? Feel free to leave a comment or ask questions.