Saturday 4 July 2009

Scaffolding, implementing and using project specific Zend_Tool_Project_Providers

Working on a project involving several legacy data migration tasks, I got curious what the Zend_Tool_Project component of the Zend Framework offers to create project specific providers for the above mentioned tasks or ones of similar nature. Therefore the following post will try to show how these providers can be developed in an iterative manner by scaffolding them via the capabilities of the Zend_Tool_Project ProjectProvider provider, enlived with action/task logic, and be used in the project scope.

Scaffolding project specific providers

All following steps assume there is a project available i.e. recordshelf initially created with the Zend_Tool_Project Project provider and that the forthcoming commands are issued from the project root directory against the zf command line client. The scaffolding of a project specific provider can be triggered via the create action of the ProjectProvider provider by passing in the name of the provider i.e. csv and it's intended actions. As the next console snippet shows it's
possible to specify several actions as a comma separated list.
sudo zf create project-provider csv importSpecials,importSummersale
After running the command the project's profile .zfproject.xml has been modified and a new providers directory exists in the project root directory containing the scaffolded Csv provider. The next code snippet shows the initial Csv provider class skeleton and its two empty action methods named importSpecials and importSummersale. At the point of this writing, using the Zend Framework 1.8.4 and PHP 5.2.10 on a Mac OS X system the generated Csv provider code or the mapping in the .zfproject.xml is incorrect, but can be fixed by renaming the class from CsvProvider to Csv.
<?php

require_once 'Zend/Tool/Project/Provider/Abstract.php';
require_once 'Zend/Tool/Project/Provider/Exception.php';

class CsvProvider extends Zend_Tool_Project_Provider_Abstract
{

public function importSpecials()
{
/** @todo Implementation */
}

public function importSummersale()
{
/** @todo Implementation */
}


}

Implementing the action logic

Having the project provider class skeleton ready to get going, it's time to enliven the actions with their intended features by using either other components of the Zend Framework, any suitable third party library or plain-vanilla PHP. For the sake of brevity I decided to implement only the importSpecials action which transforms the data of a known CSV file structure into a relevant database table. The CSV parsing steps shown next might not be that sophisticated, as their sole purpose is to illustrate an exemplary implementation of a project specific provider action.
<?php

require_once 'Zend/Tool/Project/Provider/Abstract.php';
require_once 'Zend/Tool/Project/Provider/Exception.php';

class Csv extends Zend_Tool_Project_Provider_Abstract
{
private function _isProjectProviderSupportedInProject(Zend_Tool_Project_Profile $profile,
$projectProviderName)
{
$projectProviderResource = $this->_getProjectProfileResource($profile,
$projectProviderName);
return $projectProviderResource instanceof Zend_Tool_Project_Profile_Resource;
}

private function _isActionSupportedByProjectProvider(Zend_Tool_Project_Profile $profile,
$projectProviderName, $actionName)
{
$projectProviderResource = $this->_getProjectProfileResource($profile,
$projectProviderName);
$projectProviderAttributes = $projectProviderResource->getContext()
->getPersistentAttributes();
return in_array($actionName, explode(',', $projectProviderAttributes['actionNames']));
}

private function _getProjectProfileResource(Zend_Tool_Project_Profile $profile,
$projectProviderName)
{
$profileSearchParams[] = 'ProjectProvidersDirectory';
$profileSearchParams['ProjectProviderFile'] =
array('projectProviderName' => strtolower($projectProviderName));
return $profile->search($profileSearchParams);
}

public function importSpecials($csvFile, $env = 'development')
{
$relatedTablename = 'specials';

if (!$this->_isProjectProviderSupportedInProject($profile, __CLASS__)) {
throw new Exception("ProjectProvider Csv is not supported in this project.");
}
if (!$this->_isActionSupportedByProjectProvider($profile, __CLASS__, __FUNCTION__)) {
$exceptionMessage = "Action 'importSpecials' is not supported by "
. "the Csv ProjectProvider in this project.";
throw new Exception($exceptionMessage);
}

if (!file_exists($csvFile)) {
throw new Exception("Given csv-file '{$csvFile}' doesn't exist.");
}

$importEnvironment = trim($env);
if ($importEnvironment !== 'development' && $importEnvironment !== 'production') {
throw new Exception("Unsupported environment '{$importEnvironment}' provided.");
}

$csvHandle = fopen($csvFile, "r");

if (!$csvHandle) {
throw new Exception("Unable to open given csv-file '{$csvFile}'.");
}

$config = new Zend_Config_Ini('./application/configs/application.ini',
$importEnvironment);
$db = Zend_Db::factory($config->database);

$db->query("TRUNCATE TABLE {$relatedTablename}");
echo "Truncated the project '{$relatedTablename}' database table." . PHP_EOL;

$rowCount = $insertCount = 0;

while (($csvLine = fgetcsv($csvHandle)) !== false) {
if ($rowCount > 0) {
$insertRow = array(
'product_name' => $csvLine[0],
'product_image_path' => $csvLine[1],
'price' => $csvLine[2],
'special_until' => $csvLine[3]
);
$db->insert($relatedTablename, $insertRow);
++$insertCount;
}
++$rowCount;
}
fclose($csvHandle);
$importMessage = "Imported {$insertCount} rows into the project "
. "'{$relatedTablename}' database table.";
echo $importMessage;
}

...
}

Making providers and actions pretendable

To make project specific providers its actions pretendable and thereby providing some kind of user documentation the provider classes have to implement a marker interface called Zend_Tool_Framework_Provider_Pretendable. For making a action of a provider pretendable and giving some feedback to the user, the request is checked if the action has been issued in the pretend mode; which is possible by adding -p option to the issued zf command line client command. The next code snippet shows how the above stated Csv provider and its importSpecials action is made pretendable.
<?php

require_once 'Zend/Tool/Project/Provider/Abstract.php';
require_once 'Zend/Tool/Project/Provider/Exception.php';

class Csv extends Zend_Tool_Project_Provider_Abstract implements
Zend_Tool_Framework_Provider_Pretendable

{

public function importSpecials($csvFile, $env = 'development')
{
...

if ($this->_registry->getRequest()->isPretend()) {
$pretendMessage = "I would import the specials data provided in {$csvFile} "
. "into the project '{$relatedTablename}' database table.";
echo $pretendMessage;
} else {
...
}

}
...
}

Using project specific providers

To use the bundled up capabilities of project specific providers, these have to made accessable to the zf command line client by putting them in the include_path. Currently I discovered no best practice for doing so only for single project scopes and simply added the path to the project to my php.ini and thereby global include_path; another approach might be to add the project name as a prefix to the Provider. After doing so it's possible to get an overview of all with the Zend_Tool_Project shipped providers plus the project specific providers and their offered actions by issuing the zf --help command as shown in the next screenshot. To ensure that project specific providers and its actions are only runnable in projects which support them, it is necessary to check if these and the offered action exists as resources in the project its profile .zfproject.xml file as shown in the implementation of the importSpecials action in one of above code snippets.

Provider overview

As shown in the previous screenshot the first character of the project specific providers are omitted, this is another minor bug which might be fixed in one of the forthcoming Zend Framework releases. The current workaround for this issue is simply to type the command exactly as shown in the help. The outro screenshot shows how the import-specials action of the project specific Csv provider is issued against the zf command line client and its provided user feedback after an successfull import against the projects development database.

Calling the import-specials action

No comments: