tag:blogger.com,1999:blog-84201186502360711712024-02-07T06:07:42.956+01:00<raphael.on.php/>PHP web development blog by Raphael StoltRaphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.comBlogger63125tag:blogger.com,1999:blog-8420118650236071171.post-38919520417267118092023-11-23T14:21:00.004+01:002023-11-27T14:56:07.888+01:00Automating the backslash prefixing for native PHP function callsAfter reading the blog post <a href="https://www.deviaene.eu/articles/2023/why-prefix-php-functions-calls-with-backslash/" target="_blank">Why does a backslash prefix improve PHP function call performance</a> by Jeroen Deviaene I was looking for a way to automate it for the codebase of the <a href="https://github.com/raphaelstolt/lean-package-validator">Lean Package Validator</a>, to shave off some miliseconds for it's CLI. The PHP Coding Standards Fixer has a rule named <a href="https://cs.symfony.com/doc/rules/function_notation/native_function_invocation.html">native_function_invocation</a> which does the very exact task.
<br />
<h4 class="custom">Configuring the PHP Coding Standards Fixer</h4>
<div class="refactoringStatus" style="width: 100px;">
.php-cs-fixer.php</div>
<pre class="codeSnippetRefactoring"><?php
use PhpCsFixer\Config;
use PhpCsFixer\Finder;
$finder = Finder::create()
->in([__DIR__, __DIR__ . DIRECTORY_SEPARATOR . 'tests']);
$rules = [
'psr_autoloading' => false,
'@PSR2' => true,
'phpdoc_order' => true,
'ordered_imports' => true,
<strong>'native_function_invocation' => [
'include' => ['@internal'],
'exclude' => ['file_put_contents']
]</strong>
];
$cacheDir = \getenv('HOME') ? \getenv('HOME') : __DIR__;
$config = new Config();
return $config->setRules($rules)
->setFinder($finder)
->setCacheFile($cacheDir . '/.php-cs-fixer.cache');
</pre>
To make this rule executeable I needed to add the <em class="ghl">--allow-risky=yes</em> option to the PHP Coding Standards Fixer calls in the two dedicated Composer scripts shown next.
<br /><br />
<div class="refactoringStatus" style="width: 85px;">
composer.json</div>
<pre class="codeSnippetRefactoring">"scripts": {
"lpv:test": "phpunit",
"lpv:test-with-coverage": "export XDEBUG_MODE=coverage && phpunit --coverage-html coverage-reports",
<strong>"lpv:cs-fix": "php-cs-fixer --allow-risky=yes fix . -vv || true",
"lpv:cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run --allow-risky=yes",</strong>
"lpv:configure-commit-template": "git config --add commit.template .gitmessage",
"lpv:application-version-guard": "php bin/application-version --verify-tag-match=bin",
"lpv:application-phar-version-guard": "php bin/application-version --verify-tag-match=phar",
"lpv:static-analyse": "phpstan analyse --configuration phpstan.neon.dist",
"lpv:validate-gitattributes": "bin/lean-package-validator validate"
},
</pre>
After running the <em class="ghl">lpv:cs-fix</em> Composer script the first time the tests of the system under test started failing due to <em class="ghl">file_put_contents</em> being prefixed with a backslash when using <a href="https://github.com/php-mock/php-mock" target="_blank">phpmock\MockBuilder</a>'s <em class="ghl">setName</em> method, so I had to exclude it as shown in the PHP Coding Standards Fixer configuration above.Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-24047193734571309392018-01-15T15:31:00.003+01:002023-11-23T13:35:57.927+01:00 Documenting Composer scriptsFor open source projects I'm involved with, I developed the habit to define, and document the steady growing amount of repository and build utilities via Composer <a href="https://getcomposer.org/doc/articles/scripts.md" target="_blank">scripts</a>. Having Composer scripts available makes it trivial to define aliases or shortcuts for complex and hard to remember CLI calls. It also lowers the barrier for contributors to start using these tools while helping out with fixing bugs or providing new features. Finally they're also simplifying build scripts by stashing away complexity.
<br />
<h4 class="custom">
Defining Composer scripts</h4>
If you've already defined or worked with Composer scripts or even their npm <a href="https://docs.npmjs.com/misc/scripts">equivalents</a> you can skip this section, otherwise the next code snippet allows you to study how to define these. The here defined Composer scripts range from simple CLI commands with set options (e.g. the <em class="ghl">test-with-coverage</em> script) to more complex build utility tools (i.e. the <em class="ghl">application-version-guard</em> script) which are extracted into specific CLI commands to avoid cluttering up the <em class="ghl">composer.json</em> or even the <em class="ghl">.travis.yml</em>.<br />
<br />
<div class="refactoringStatus" style="width: 90px;">
composer.json</div>
<pre class="codeSnippetRefactoring">{
"scripts": {
"test": "phpunit",
<strong>"test-with-coverage": "phpunit --coverage-html coverage-reports",</strong>
"cs-fix": "php-cs-fixer fix . -vv || true",
"cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
"configure-commit-template": "git config --add commit.template .gitmessage",
<strong>"application-version-guard": "php bin/application-version --verify-tag-match"</strong>
}
}
</pre>
<h4 class="custom">
Describing Composer scripts</h4>
Since Composer <a href="https://github.com/composer/composer/releases/tag/1.6.0" target="_blank">1.6.0</a> it's possible to set custom script descriptions via the <em class="ghl">scripts-descriptions</em> element like shown next. It's to point out here that the name of a description has to match the name of a defined <strong>custom</strong> Composer script to be recognised at runtime. On another note it's to mention that the description should be worded in simple present to align with the other Composer command descriptions.<br />
<br />
<div class="refactoringStatus" style="width: 90px;">
composer.json</div>
<pre class="codeSnippetRefactoring">{
"scripts-descriptions": {
"test": "Runs all tests.",
"test-with-coverage": "Runs all tests and measures code coverage.",
"cs-fix": "Fixes coding standard violations.",
"cs-lint": "Checks for coding standard violations.",
<strong>"configure-commit-template": "Configures a local commit message template.",</strong>
"application-version-guard": "Checks that the application version matches the given Git tag."
},
"scripts": {
"test": "phpunit",
"test-with-coverage": "phpunit --coverage-html coverage-reports",
"cs-fix": "php-cs-fixer fix . -vv || true",
"cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
<strong>"configure-commit-template": "git config --add commit.template .gitmessage",</strong>
"application-version-guard": "php bin/application-version --verify-tag-match"
}
}
</pre>
Now when running <em class="ghl">$ composer</em> via the terminal the descriptions of defined custom scripts will show up sorted in into the list of available commands, which makes it very hard to spot the Composer scripts of the package at hand. Luckily Composer scripts can also be namespaced.<h4 class="custom">
Namespacing Composer scripts</h4>To namespace (i.e. <em class="ghl">some-namespace</em>) the custom Composer scripts for any given package define the script names with a namespace prefix as shown next. As the chances are very high that you will be using the one or other Composer script several times, while working on the package, it's recommended to use a short namespace like in the range from two to four characters.<br /><br /><div class="refactoringStatus" style="width: 90px;">
composer.json</div>
<pre class="codeSnippetRefactoring">{
"scripts": {
"some-namespace:test": "phpunit",
"some-namespace:test-with-coverage": "phpunit --coverage-html coverage-reports",
"some-namespace:cs-fix": "php-cs-fixer fix . -vv || true",
"some-namespace:cs-lint": "php-cs-fixer fix --diff --stop-on-violation --verbose --dry-run",
"some-namespace:configure-commit-template": "git config --add commit.template .gitmessage",
"some-namespace:application-version-guard": "php bin/application-version --verify-tag-match"
}
}
</pre>Now this time when running <em class="ghl">$ composer</em> via the terminal the defined custom scripts will show up in the list of available commands in a <em>namespaced</em> manner giving an immediate overview of the available Composer script of the package at hand.<br /><br /><pre class="codeSnippetRefactoring">$ composer
<strong>... ommitted content</strong>
Available commands:
<strong>... ommitted content</strong>
some-namespace
some-namespace:application-version-guard Checks that the application version matches the given Git tag.
some-namespace:configure-commit-template Configures a local commit message template.
some-namespace:cs-fix Fixes coding standard violations.
some-namespace:cs-lint Checks for coding standard violations.
some-namespace:test Runs all tests.
some-namespace:test-with-coverage Runs all tests and measures code coverage.
</pre>To use any namespaced Composer script, e.g. to fix coding standard violations after a substantial refactoring, it has to be called with its namespace e.g.<em class="ghl">$ composer some-namespace:cs-fix</em>, which is the one disadavantage of Composer script namespacing.Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com1tag:blogger.com,1999:blog-8420118650236071171.post-73841457118879155712017-03-25T19:50:00.000+01:002017-03-25T19:50:27.184+01:00Keeping your CLI integration tests green on WindowsLately on a Windows system, some failing integration tests for CLI commands utilising the <a href="http://symfony.com/doc/current/components/console.html" target="_blank">Symfony Console</a> component caused me some blip headaches by PHPUnit insisting that two strings are not identical due to different line endings. The following post documents the small steps I took to overcome these headaches.</p>
<p>First the assertion message produced by the failing test, see the console output below, got me thinking it might be caused by different encodings and line endings; though the project was utilising an <a href="http://editorconfig.org" target="_blank">.editorconfig</a> from the early start and the related files were all encoded correctly and had the configured line endings. The Git configuration <em class="ghl">e.g. core.autocrlf=input</em> also was as it should be.<pre class="codeSnippetRefactoring">1) Stolt\LeanPackage\Tests\Commands\InitCommandTest::createsExpectedDefaultLpvFile
Failed asserting that two strings are identical.
--- Expected
+++ Actual
@@ @@
<strong> #Warning: Strings contain different line endings!</strong>
-Created default 'C:\Users\stolt\AppData\Local\Temp\lpv\.lpv' file.
+Created default 'C:\Users\stolt\AppData\Local\Temp\lpv\.lpv' file.</pre>Another deeper look at the <a href="http://api.symfony.com/3.2/Symfony/Component/Console/Tester/CommandTester.html" target="_blank">CommandTester</a> class yielded that it’s possible to disable the command output decoration and also to normalise the command output. So a change of the SUT preparation and a normalisation of the console output, visualised via a <em class="ghl">git diff -U10</em>, brought the solution for this particular test.<br /><br /><pre class="codeSnippetRefactoring">diff --git a/tests/Commands/InitCommandTest.php b/tests/Commands/InitCommandTest.php
index 58e7114..fb406f3 100644
--- a/tests/Commands/InitCommandTest.php
+++ b/tests/Commands/InitCommandTest.php
@@ -48,21 +48,21 @@ class InitCommandTest extends TestCase
/**
* @test
*/
public function createsExpectedDefaultLpvFile()
{
$command = $this->application->find('init');
$commandTester = new CommandTester($command);
$commandTester->execute([
'command' => $command->getName(),
'directory' => WORKING_DIRECTORY,
- ]);
<strong>+ ], ['decorated' => false]);</strong>
// ommitted code
- $this->assertSame($expectedDisplay, $commandTester->getDisplay());
<strong>+ $this->assertSame($expectedDisplay, $commandTester->getDisplay(true));</strong>
$this->assertTrue($commandTester->getStatusCode() == 0);
$this->assertFileExists($expectedDefaultLpvFile);</pre>Since the SUT had a <strong>lot</strong> of integration test for its CLI commands, the lazy me took the shortcut to extend the <em class="ghl">CommandTester</em> and using it, with desired defaults set, instead of changing all of the related command instantiations.<br /><br /><pre class="codeSnippetRefactoring"><?php
namespace SUT\Tests;
use Symfony\Component\Console\Tester\CommandTester as ConsoleCommandTester;
class CommandTester extends ConsoleCommandTester
{
/**
* Gets the display returned by the last execution of the command.
*
* @param bool $normalize Whether to normalize end of lines to \n or not
*
* @return string The display
*/
public function getDisplay(<strong>$normalize = true</strong>)
{
return parent::getDisplay($normalize);
}
/**
* Executes the command.
*
* Available execution options:
*
* * interactive: Sets the input interactive flag
* * decorated: Sets the output decorated flag
* * verbosity: Sets the output verbosity flag
*
* @param array $input An array of command arguments and options
* @param array $options An array of execution options
*
* @return int The command exit code
*/
public function execute(
array $input,
<strong>array $options = ['decorated' => false]</strong>
) {
return parent::execute($input, $options);
}
}</pre>So it's a yay for green CLI command integration tests on Windows from here on. Another measure for the SUT would be to enable Continuous Integration on a Windows system via <a href="https://www.appveyor.com" target="_blank">AppVeyor</a>, <strike>but that’s a task for another commit</strike> <a href="https://github.com/raphaelstolt/lean-package-validator/commit/85bdf22b0e629fb70879ebde50143fc5862f5f49" target="_blank">85bdf22</a>.Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-48501934835036580192016-10-10T23:02:00.000+02:002016-10-27T09:44:59.956+02:00Eight knobs to adjust and improve your Travis CI buildsAfter having refactored several <a href="https://travis-ci.org" target="_blank">Travis CI</a> configuration files over the last weeks, this post will provide eight adjustments or patterns immediately applicable for faster, changeable, and economic builds.
<h4 class="custom">1. Reduce git clone depth</h4>
The first one is a simple configuration addition with a positive impact on the time and disk space consumption, which should be quite noticeable on larger code bases. Having this configured will enable shallow clones of the Git repository and reduce the clone depth from 50 to 2.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
git:
depth: <b>2</b>
</pre>
<h4 class="custom">2. Enable caching</h4>
The second one is also a simple configuration addition for caching the Composer dependencies of the system under build (SUB) or its result of static code analysis. Generally have a look if your used tools allow caching and if so cache away. This one deserves a shout out to <a href="https://twitter.com/localheinz" target="_blank">@localheinz</a> for teaching me about this one.
<br /><br />
The next shown configuration excerpt assumes that you lint coding standard compliance with the PHP Coding Standards Fixer in version <em class="ghl">2.0.0-alpha</em> and have enable caching in its <em class="ghl">.php_cs</em> configuration.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
cache:
directories:
- $HOME/.composer/cache
- $HOME/.php-cs-fixer
</pre>
<h4 class="custom">3. Enforce contribution standards</h4>
This one might be a tad controversial, but after having had the joys of merging GitHub pull requests from a master branch I started to fail builds <b>not</b> coming from <a href="https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows" target="_blank">feature or topic branch</a> with the next shown bash script. It's residing in an external bash script to avoid the <a href="https://docs.travis-ci.com/user/customizing-the-build/#How-does-this-work%3F-(Or%2C-why-you-should-not-use-exit-in-build-steps)" target="_blank">risk</a> of terminating the build process.<br /><br /><div class="refactoringStatus" style="width: 330px;">./bin/travis/fail-non-feature-topic-branch-pull-request</div><pre class="codeSnippetRefactoring">
#!/bin/bash
set -e
if [[ <b>$TRAVIS_PULL_REQUEST_BRANCH = master</b> ]]; then
echo "Please open pull request from a feature / topic branch.";
exit 1;
fi
</pre><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
script:
- ./bin/travis/fail-non-feature-topic-branch-pull-request
</pre>The bash script could be extended to fail pull requests not following a branch naming scheme, e.g. <em class="ghl">feature-</em> for feature additions or <em class="ghl">fix-</em> for bug fixes, by evaluating the branch name. If this is a requirement for your builds you should also look into the <a href="https://docs.travis-ci.com/user/customizing-the-build#Using-regular-expressions" target="_blank">blocklisting branches</a> feature of Travis CI.
<h4 class="custom">4. Configure PHP versions in an include</h4>
With configuring the PHP versions to build against in a <b>matrix include</b> it's much easier to inject enviroment variables and therewith configure the version specific build steps. You can even set multiple enviroment variables like done on the 7.0 version.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
env:
global:
- OPCODE_CACHE=apc
matrix:
include:
- php: hhvm
- php: nightly
- php: 7.1
- php: 7.0
env: <b>DISABLE_XDEBUG=true</b> LINT=true
- php: 5.6
env:
- <b>DISABLE_XDEBUG=true</b>
before_script:
- if [[ <b>$DISABLE_XDEBUG = true</b> ]]; then
phpenv config-rm xdebug.ini;
fi
</pre>I don't know if enviroment variable injection is also possible with the <a href="https://docs.travis-ci.com/user/languages/php/#Choosing-PHP-versions-to-test-against" target="_blank">minimalistic way</a> to define the PHP versions list, so you should take that adjustment with a grain of salt.<br /><br />
It also seems like I stumbled upon a Travis CI <a href="https://github.com/travis-ci/travis-ci/issues/6703" target="_blank">bug</a> where the global enviroment variable <em class="ghl">OPCODE_CACHE</em> is lost, so add <b>another</b> grain of salt. To work around that possible bug the relevant configuration has to look like this, which sadly adds some duplication and might be unsuitable when dealing with a large amount of environment variables.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
matrix:
include:
- php: hhvm
env:
- OPCODE_CACHE=apc
- php: nightly
env:
- OPCODE_CACHE=apc
- php: 7.1
env:
- OPCODE_CACHE=apc
- php: 7.0
env: OPCODE_CACHE=apc <b>DISABLE_XDEBUG=true</b> LINT=true
- php: 5.6
env: OPCODE_CACHE=apc <b>DISABLE_XDEBUG=true</b>
before_script:
- if [[ <b>$DISABLE_XDEBUG = true</b> ]]; then
phpenv config-rm xdebug.ini;
fi
</pre>
<h4 class="custom">5. Only do static code analysis or code coverage measurement once</h4>
This one is for reducing the build duration and load by avoiding unnecessary build step repetition. It's achived by linting against coding standard violations or generating the code coverage for just a <b>single</b> PHP version per build, in most cases it will be the same for 5.6 or 7.0.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
matrix:
include:
- php: hhvm
- php: nightly
- php: 7.1
- php: 7.0
env: DISABLE_XDEBUG=true <b>LINT=true</b>
- php: 5.6
env:
- DISABLE_XDEBUG=true
script:
- if [[ <b>$LINT=true</b> ]]; then
composer cs-lint;
composer test-test-with-coverage;
fi</pre>
<h4 class="custom">6. Only do release releated analysis and checks on tagged builds</h4>
This one is also for reducing the build duration and load by targeting the analysis and checks on tagged release builds. For example if you want to ensure that your CLI binary version, the one produced via the <em class="ghl">--version</em> option, matches the Git repository version tag run this check only on tagged builds.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
script:
- if [[ <b>! -z "$TRAVIS_TAG"</b> ]]; then
composer application-version-guard;
fi</pre>
<h4 class="custom">7. Run integration tests on very xth build</h4>
For catching breaking changes in interfaces or API's beyond your control it makes sense do run integration tests against them once in a while, but <b>not</b> on every single build, like shown in the next Travis CI configuration excerpt which runs the integration tests on every 50th build.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">
script:
- if [[ <b>$(( $TRAVIS_BUILD_NUMBER % 50 )) = 0</b> ]]; then
composer test-all;
else
composer test;
fi</pre>
<h4 class="custom">8. Utilise Composer scripts</h4>
The last one is all about improving the readability of the Travis CI configuration by extracting command configurations i.e. options into dedicated <a href="https://getcomposer.org/doc/articles/scripts.md" target="_blank">Composer scripts</a>. This way the commands are also available during your development activitives and not hidden away in the <em class="ghl">.travis.yml</em> file.<br /><br /><div class="refactoringStatus" style="width: 90px;">composer.json</div><pre class="codeSnippetRefactoring">
{
"__comment": "omitted other configuration",
"scripts": {
"test": "phpunit",
"test-with-coverage": <b>"phpunit --coverage-html coverage-reports"</b>,
"cs-fix": "php-cs-fixer fix . -vv || true",
"cs-lint": "php-cs-fixer fix --diff --verbose --dry-run"
}
}
</pre>To ensure you don't end up with an invalid Travis CI configuration, which might be accidently committed, you can use <a href="https://github.com/raphaelstolt/composer-travis-lint" target="_blank">composer-travis-lint</a> a simple Composer script linting the <em class="ghl">.travis.yml</em> with the help of the <a href="https://docs.travis-ci.com/api#linting" target="_blank">Travis CI API</a>.
<br /><br />
Happy refactoring.
Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-67666501356538695162016-09-20T19:32:00.001+02:002016-10-27T09:41:09.187+02:00Anatomy of a dope PHP package repositoryWhile contributing to <a href="https://git.io/construct">Construct</a>, maintained by <a href="https://twitter.com/torres_jonathan">Jonathan Torres</a>, I gathered some insights and learnings on the characteristics of a dope PHP package repository. This post summarises and illustrates these, so that PHP package develeopers have a complementary <i>guideline</i> to improve existing or imminent package repositories. <a href="https://twitter.com/reinink">Jonathan Reinink</a> did a good job in putting the <a href="http://phppackagechecklist.com/">PHP package checklist</a> out there which provides an incomplete, but solid quality checklist for open-source PHP packages.
<br /><br />
I'll distill the characteristics of a dope PHP package repository by looking at the repository artifacts <i>Construct</i> can generate for you when starting the development of a new PHP project or micro-package. The following <em class="ghl">tree</em> command output shows most of the elements this post will touch upon. The artifacts in parenthese are optional and configurable from <i>Construct</i> but can nonetheless have an import impact on the overall package quality.<pre class="codeSnippet">
├── <package-name>
│ ├── CHANGELOG.md
│ ├── (CONDUCT.md)
│ ├── composer.json
│ ├── composer.lock
│ ├── CONTRIBUTING.md
│ ├── (.editorconfig)
│ ├── (.env)
│ ├── (.env.example)
│ ├── (.git)
│ │ └── ...
│ ├── .gitattributes
│ ├── (.github)
│ │ ├── CONTRIBUTING.md
│ │ ├── ISSUE_TEMPLATE.md
│ │ └── PULL_REQUEST_TEMPLATE.md
│ ├── .gitmessage
│ ├── .gitignore
│ ├── (.lgtm)
│ ├── LICENSE.md
│ ├── (MAINTAINERS)
│ ├── (.php_cs)
│ ├── (phpunit.xml.dist)
│ ├── README.md
│ ├── (docs)
│ │ └── index.md
│ ├── src
│ │ └── Logger.php
│ ├── tests
│ │ └── LoggerTest.php
│ ├── .travis.yml
│ ├── (Vagrantfile)
│ └── vendor
│ └── ...</pre>
<h4 class="custom">Definition of a dope PHP package repository</h4>
Before jumping into the details, let's define what could be considered as a dope package repository. Therefor, being lazy, I'm going to simply reword this classic quote from Michael Feathers
<blockquote class="em">> Clean code is code that is written by someone who cares.</blockquote>
to
<blockquote class="em">> A dope PHP package repository is one that is created and maintained by someone who cares.</blockquote>
<h4 class="custom">Artifact categories</h4>
The next shown pyramid illustrates the three main categories the artifacts of a package repository will fall into.
<div class="separator" style="clear: both; text-align: center;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5k8D7JFKfOC2oUsN3ZEyXYx-QWAzkHyPqnYOtpLpzcjx0B-9oWWS-xZFUqIeqY79xBGojWRNDqRZccYmW7VTXU_fiaGiwOgG8SmFhdTtdKaHNSjyFDneohQVVozgbBGUcVGzRImiNx24/s1600/categories.gif" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg5k8D7JFKfOC2oUsN3ZEyXYx-QWAzkHyPqnYOtpLpzcjx0B-9oWWS-xZFUqIeqY79xBGojWRNDqRZccYmW7VTXU_fiaGiwOgG8SmFhdTtdKaHNSjyFDneohQVVozgbBGUcVGzRImiNx24/s400/categories.gif" width="400" height="327" /></a></div>
First and most important there's the main <b>sourcecode</b>, it's <b>tests</b> or <b>specs</b>, and the <b>documentation</b> which could be dependent on it's size reside in a <em class="ghl">README.md</em> section or inside a dedicated <em class="ghl">docs</em> directory. Using a <em class="ghl">docs</em> directory also allows <a href="(https://github.com/blog/2233-publish-your-project-documentation-with-github-pages">publishing</a> the documentation via GitHub pages. Other aspects of a package which <b>should</b> be documented are the chosen <i>license</i>, how to <i>contribute</i> to the package, possibly a <i>code of conduct</i> to comply with, and the <i>changes</i> made over the lifespan of the package.<br /><br />
Second there's the <b>configuration</b> for a myriad of tools like <a href="https://git-scm.com/">Git</a>, <a href="https://github.com/">GitHub</a>, <a href="http://editorconfig.org/">EditorConfig</a>, <a href="https://getcomposer.org/">Composer</a>, the preferred testing framework, the preferred continuous inspection / integration platform such like <a href="https://scrutinizer-ci.com/">Scrutinizer</a> or <a href="https://docs.travis-ci.com/">Travis CI</a>, and so forth.<br /><br />
The final category includes <b>tools</b> which ease the life of <i>maintainers</i> and potential <i>contributors</i> equally. These tools can be helpful for <i>releasing</i> new versions, enforcing <i>coding standard</i> compliance, or <i>commit message</i> quality and consistency.
<h4 class="emulateH3">Consistency</h4>
<h4 class="custom">Sourcecode</h4>
All sourcecode and accompanying tests or specs should follow a coding standard (<a href="http://www.php-fig.org/psr/psr-2/">PSR-2</a>) and have a consistent formatting style, there's nothing new here. The perfect place to communicate such requirements is the <em class="ghl">CONTRIBUTING.md</em> file.<br /><br />
Tools like <a href="http://cs.sensiolabs.org/">PHP Coding Standards Fixer</a> or <a href="https://github.com/squizlabs/PHP_CodeSniffer">PHP_CodeSniffer</a> in combination with a present configuration <em class="ghl">.php_cs|ruleset.xml.dist</em> and a command wrapping <a href="https://getcomposer.org/doc/articles/scripts.md#defining-scripts">Composer script</a> are an ideal match to ease compliance. The Composer script <em class="ghl">cs-fix</em> shown next will be available for maintainers and contributors alike.<br /><br /><div class="refactoringStatus" style="width: 90px;">composer.json</div><pre class="codeSnippetRefactoring">
{
"__comment": "omitted other configuration",
"scripts": {
<b>"cs-fix": "php-cs-fixer fix . -vv || true"</b>
}
}
</pre>Consistent formatting styles like line endings, indentation style, and file encoding can be configured via an <a href="http://editorconfig.org/">EditorConfig</a> configuration residing in <em class="ghl">.editorconfig</em> which will be used when supported by the IDE or text editor of choice.
<h4 class="custom">Artifact naming and casing</h4>
Like sourcecode formatting and naming, repository artifacts should also follow a predictable naming scheme. All documentation files should have a consistent extension like <em class="ghl">.md</em> or <em class="ghl">.rst</em> and the casing should be consistent throughout the package repository.
Comparing<pre class="codeSnippet">├── <package-name>
│ ├── changelog.md
│ ├── code_of_conduct.md
│ ├── ...
│ ├── .github
│ │ └── ...
│ ├── LICENSE
│ ├── Readme.md
│ ├── roadmap.rst</pre>
to<pre class="codeSnippet">├── <package-name>
│ ├── CHANGELOG.md
│ ├── CODE_OF_CONDUCT.md
│ ├── ...
│ ├── .github
│ │ └── ...
│ ├── LICENSE.md
│ ├── README.md
│ ├── ROADMAP.md</pre>I would favour the later one anytime for it's much easier reading flow and pattern matchableness. The easier reading flow is achieved by the upper casing of the <em class="ghl">*.md</em> files which also clearly communicates their documentation character.<br /><br />The configuration files for tools which except the <em class="ghl">.dist</em> file extension per default <b>should</b> all have such one like shown next.<pre class="codeSnippet">├── <package-name>
│ ├── build.xml.dist
│ ├── phpunit.xml.dist
│ ├── ruleset.xml.dist
│ ├── ...</pre>
<h4 class="custom">Commit message format</h4>
Next to the package's changelog, incrementally growing in the <em class="ghl">CHANGELOG.md</em> file, the Git commit messages are an important source of change communication. Therefor they <b>should</b> also follow a consistent format which improves the reading flow while also leaving a professional impression. This format can be documented once again in the <em class="ghl">CONTRIBUTING.md</em> file or even better be provided via a <em class="ghl">.gitmessage</em> file residing in the package's Git repository.<br /><br />
Once more a Composer script, named <em class="ghl">configure-commit-template</em> here, can ease configuration and if configured Git will use it's content when committing without the <em class="ghl">-m|--message</em> and <em class="ghl">-F|--file</em> option.<br /><br /><div class="refactoringStatus" style="width: 90px;">composer.json</div><pre class="codeSnippetRefactoring">{
"__comment": "omitted other configuration",
"scripts": {
<b>"configure-commit-template": "git config --add commit.template .gitmessage"</b>
}
}</pre>To enforce <b>commit message formatting</b> adherence to the <a href="http://chris.beams.io/posts/git-commit/">rules</a> described by Chris Beams on a Git hook level, the <a href="https://github.com/PurpleBooth/git-lint-validators">git-lint-validators</a> utility by <a href="https://twitter.com/BillieCodes">Billie Thompson</a> can be helpful.
<h4 class="custom">Versioning</h4>
Release versions should follow the <a href="http://semver.org/">semantic versioning specification</a> aka SemVer, once again there's nothing new here. When using version numbers in the sourcecode or CLI binaries, these should be in sync with the set Git tag. Tools like <a href="https://github.com/liip/RMT">RMT</a> or self-written tools <b>should</b> be utilised for this mundane task.<br /><br />
The next shown code illustrates such a <i>simple</i> self-written tool named <em class="ghl">application-version</em>. It's main purpose is to set the provided version number in the CLI application's binary and avoid an application version and Git tag mismatch.<br /><br /><div class="refactoringStatus" style="width: 140px;">bin/application-version</div><pre class="codeSnippetRefactoring">#!/usr/bin/env php
<?php
$binApplicationName = '<bin-application-name>';
$binFile = __DIR__ . DIRECTORY_SEPARATOR . $binApplicationName;
list($void, $binFileRelative) = explode($binApplicationName, $binFile, 2);
$shortBinFilePath = $binApplicationName . $binFileRelative;
$options = getopt('v:ch', ['version:', 'current', 'verify-tag-match', 'help', 'current-raw']);
$help = <<<HELP
This command sets the version number in the {$shortBinFilePath} file:
Usage:
application-version [options]
Options:
-c, --current, --current-raw The current version number
<b>--verify-tag-match Verify application version and Git tag match</b>
<b>-v, --version The version number to set</b>
-h, --help Display this help message
HELP;
if (array_key_exists('h', $options) || array_key_exists('help', $options)) {
echo $help;
exit(0);
}
/**
* Return the application version.
*
* @param string $binFile File holding the application version.
* @return string
*/
function get_application_version($binFile) {
$matches = [];
$match = preg_match(
'/(\d+\.)?(\d+\.)?(\*|\d+)/',
file_get_contents($binFile),
$matches
);
return trim($matches[0]);
}
/**
* Return latest tagged version.
*
* @return string
*/
function get_latest_tagged_version() {
exec('git describe --tags --abbrev=0', $output);
return trim($output[0]);
}
<b>if (array_key_exists('verify-tag-match', $options)) {
$applicationVersion = 'v' . get_application_version($binFile);
$latestGitTag = get_latest_tagged_version();
if ($applicationVersion === $latestGitTag) {
echo "The application version and Git tag match on {$latestGitTag}." . PHP_EOL;
exit(0);
}
echo "The application version {$applicationVersion} and Git tag {$latestGitTag} don't match." . PHP_EOL;
exit(1);
}</b>
if (array_key_exists('current-raw', $options)) {
echo get_application_version($binFile) . PHP_EOL;
exit(0);
}
if (array_key_exists('c', $options) || array_key_exists('current', $options)) {
$applicationVersion = 'v' . get_application_version($binFile);
$latestGitTag = get_latest_tagged_version();
echo "Current version set in {$shortBinFilePath} is {$applicationVersion}." . PHP_EOL;
echo "Current tagged version {$latestGitTag}." . PHP_EOL;
exit(0);
}
if ($options === []) {
echo 'No options set.' . PHP_EOL;
exit(1);
}
<b>$version = isset($options['version']) ? trim($options['version']) : trim($options['v']);
$fileContent = file_get_contents($binFile);
$fileContent = preg_replace(
'/(.*define.*VERSION.*)/',
"define('VERSION', '$version');",
$fileContent
);
file_put_contents($binFile, $fileContent);
echo "Set version in {$shortBinFilePath} to {$version}." . PHP_EOL;
exit(0);</b></pre>
The <em class="ghl">application-version</em> tool could further be utilised in Travis CI builds, to avoid the earlier mentioned version differences, like shown in the next <em class="ghl">.travis.yml</em> diggest. On an application version and Git tag <b>mismatch</b> the shown build script will break the build early.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">language: php
# omitted other configuration
script:
# Verify application version and Git tag match
- <b>php bin/application-version --verify-tag-match</b>
# omitted other scripts</pre>
<h4 class="custom">Lean builds</h4>
To speed up continuous integration builds, resource and time consuming extensions like Xdebug should be disabled when <b>not</b> required for measuring code coverage. The next shown before_script, tailored for Travis CI, is generated by <i>Construct</i> per default and might shave off a few build seconds and thereby provide a faster feedback.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">language: php
# omitted other configuration
before_script:
- <b>phpenv config-rm xdebug.ini || true</b>
# omitted other before_scripts</pre>
To reduce email traffic, the email notifications send by Travis CI should be reduced to a minimum like shown next, or dependent on your workflow the could be disabled at all.<br /><br /><div class="refactoringStatus" style="width: 70px;">.travis.yml</div><pre class="codeSnippetRefactoring">language: php
# omitted other configuration
notifications:
email:
on_success: never</pre>
Something I really would love to be supported by Travis CI is a feature to ignore a set of definable artifacts which could be configured in a <em class="ghl">.buildignore</em> file or the like. This way wording or spelling changes on non build relevant artifacts like the <em class="ghl">README.md</em> wouldn't trigger a build and <i>misspend</i> <b>resources</b> and <b>energy</b>. There's a <a href="https://github.com/travis-ci/travis-ci/issues/1468">related</a> GitHub issue and here's hope it will be revisited in the near future.
<h4 class="custom">Lean releases</h4>
To keep releases (or dists in Composer lingo) of PHP projects or micro-packages as lean as possible, their repositories should contain a complete and valid <em class="ghl">.gitattributes</em> file. With such a file present all <b>export-ignored</b> files will be excluded from release archives and thereby <i>save</i> a significant amount of <b>bandwith</b> and <b>energy</b>.<br /><br />
The next code shows the content of such a <em class="ghl">.gitattributes</em> file excluding non release relevant files like <em class="ghl">internal tools</em>, <em class="ghl">configuration</em>, and <em class="ghl">documentation</em> artifacts. If for some reasons you require the complete source of a PHP project or micro-package you can bypass the default by using Composer's <em class="ghl">--prefer-source</em> option.<br /><br /><div class="refactoringStatus" style="width: 80px;">.gitattributes</div><pre class="codeSnippetRefactoring">* text=auto eol=lf
.editorconfig export-ignore
.gitattributes export-ignore
.github/ export-ignore
.gitignore export-ignore
.gitmessage export-ignore
.php_cs export-ignore
.travis.yml export-ignore
bin/application-version export-ignore
bin/release-version export-ignore
bin/start-watchman export-ignore
CHANGELOG.md export-ignore
LICENSE.md export-ignore
phpunit.xml.dist export-ignore
README.md export-ignore
tests/ export-ignore</pre>To validate the <em class="ghl">.gitattributes</em> file of a PHP project or micro-package on the repository, Git HEAD, or build level the <a href="https://github.com/raphaelstolt/lean-package-validator">LeanPackageValidator</a> CLI can be helpful.
<h4 class="custom">Avoid badge posing</h4>
Badges, if used sparsely, are a handy tool for visualising some of the repository properties. It's definitely nice to immediately see the required PHP version, the current build status, or the latest version of the package as they <b>save</b> you manual look ups.<br /><br />Badges showing the amount of downloads, code coverage, or the chosen license are in my opinion kind of poserish, they cause unnecessary requests to the badge service, and are in case of the license even obsolete.
<h4 class="custom">Why should you care about the dopeness of a PHP package repository?</h4>
Creating and maintaining a dope PHP package repository might have a positive impact on several levels. It can earn you some valuable Brownie points or even provide a conversation gambit when doing job interviews, simply because you showcase professionalism.<br /><br /> Furthermore it's more likely to get valuable and high quality contributions from your second target audience when supportive documentation and tooling for things like issue creation, coding standards compliance, or Git commit message consistency are available.<br /><br />It also might convince an end user, your main target audience, in using your package over a <i>competitive</i> one.
<h4 class="custom">Le fini</h4>
So these were my recent insights and learnings on the anatomy of a dope PHP package repository. If you'r lucky to be attending this year's ZendCon, I recommend to catch Matthew Weier O'Phinney's session about <a href="http://www.zendcon.com/session/create-phpantastic-packages">Creating PHPantastic packages</a>. I definitely be waiting for the related slides.<br /><br /><i>Happy packaging.</i>
Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-804523184592944802011-04-22T17:44:00.001+02:002011-11-09T12:41:10.122+01:00Enforcing target descriptions within build files with a Git hookWhen automating mundane tasks of a project or development environment with a build tool like <a href="http://phing.info/">Phing</a> or <a href="http://ant.apache.org/">Ant</a>, the driving build file will naturally accumulate several targets and tasks over time. To ease the build file acceptance within a team and at a later stage also the contribution rate by team members, it's crucial that all build targets have a description attribute to provide at least a rough outline of the build features at hand. When these attributes are in place the (potential) build file user will get such an outline by executing the build tool's list command (<em class="command">phing -l</em> or <em class="command">ant -p</em>). To get a better picture of the problem at hand imagine a project poorly covered with tests and your personal attitude towards extending it or just take a peek at the screenshot below showing a very poorly documented build file.<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/5643674328/" title="A poorly documented build file in Phing's list view von Raphael Stolt bei Flickr"><img alt="A poorly documented build file in Phing's list view" border="0" height="314" src="http://farm6.static.flickr.com/5026/5643674328_2e293600a6.jpg" width="500" /></a><br />
<br />
To overcome this accumulation of some sort of technical debt (i.e. poorly documented targets) there are various options at hand. The first one, not covered in this blog post, would be to add a pursuant test which verifies the existence of a description for every target/task of the build file under test. As it's very uncommon, at least from what I've heard, to have your build files covered by tests; the next thinkable approach would be to use a <a href="http://git-scm.com/">Git</a> pre-commit hook to guard your repository/ies against the creeping in of such poorly documented build files.<br />
<br />
The next listing shows such a Git hook (also <a href="https://gist.github.com/936903">available</a> via GitHub) scribbled away in PHP, which detects any build file(s) following a common build file naming schema (i.e. build.xml|build.xml.dist|personal-build.xml|…) , prior to the actual commit. For every target element in the detected build file(s) it's then verified that it has a description attribute and that it's actual content is long enough to carry some meaning. If one of those two requirements aren't met, the commit is rejected while revealing the build file smells to the committer, so she can fix it, as shown in the outro screenshot. Happy build file sniffing.<br />
<pre class="codeSnippet">#!/usr/bin/php
<?php
define('DEPENDENT_EXTENSION', 'SimpleXML');
if (!extension_loaded(DEPENDENT_EXTENSION)) {
$consoleMessage = sprintf(
"Skipping build file checks as the '%s' extension isn't available.",
DEPENDENT_EXTENSION
);
echo $consoleMessage . PHP_EOL;
exit(0);
}
define('MIN_TARGET_DESCRIPTION_LENGTH', 10);
define('TARGET_DESCRIPTION_ATTRIBUTE', 'description');
define('TARGET_NAME_ATTRIBUTE', 'name');
define('CHECK_DESCRIPTION_LENGTH', true);
$possibleBuildFileNames = array(
'build.xml.dist',
'build.xml-dist',
'build-dist.xml',
'build.xml',
'personal-build.xml'
);
$violations = getAllBuildFileViolationsOfCommit($possibleBuildFileNames);
fireBackPossibleViolationsAndExitAccordingly($violations);
function getAllBuildFileViolationsOfCommit(array $possibleBuildFileNames)
{
$filesOfCommit = array();
$gitCommand = 'git diff --cached --name-only';
exec($gitCommand, $filesOfCommit, $commandReturnCode);
$allViolations = array();
foreach ($filesOfCommit as $file) {
if (in_array(basename($file), $possibleBuildFileNames)) {
$violations = checkBuildFileForViolations($file);
if (count($violations) > 0) {
$allViolations[$file] = $violations;
}
}
}
return $allViolations;
}
/**
* @param array $allViolations
* @return void
*/
function fireBackPossibleViolationsAndExitAccordingly(array $allViolations)
{
if (count($allViolations) > 0) {
foreach ($allViolations as $buildFile => $violations) {
$buildFileConsoleMessageHeader = sprintf("Build file '%s':", $buildFile);
echo $buildFileConsoleMessageHeader . PHP_EOL;
foreach ($violations as $violationMessage) {
$buildFileConsoleMessageLine = sprintf(" + %s", $violationMessage);
echo $buildFileConsoleMessageLine . PHP_EOL;
}
}
if (count($allViolations) > 1) {
$rejectCommitConsoleMessage = sprintf(
"Therefore rejecting the commit of build files [ %s ].",
implode(', ', array_keys($allViolations))
);
} else {
$rejectCommitConsoleMessage = sprintf(
"Therefore rejecting the commit of build file [ %s ].",
implode(', ', array_keys($allViolations))
);
}
echo $rejectCommitConsoleMessage . PHP_EOL;
exit(1);
}
exit(0);
}
/**
* @param string $buildfile
* @return array
*/
function checkBuildFileForViolations($buildFile) {
if (!file_exists($buildFile)) {
return array();
}
$buildfileXml = file_get_contents($buildFile);
$buildXml = new SimpleXMLElement($buildfileXml);
$allBuildTargets = $buildXml->xpath("//target");
$violations = array();
if (count($allBuildTargets) > 0) {
$targetsWithNoDescription = $targetsWithTooShortDescription = array();
foreach ($allBuildTargets as $buildTarget) {
$actualTragetAttributes = $buildTarget->attributes();
$allUsedTragetAttributes = array();
$actualTargetName = null;
foreach ($actualTragetAttributes as $attribute => $value) {
$allUsedTragetAttributes[] = $attribute;
if ($attribute === TARGET_NAME_ATTRIBUTE) {
$actualTargetName = $value;
}
if (CHECK_DESCRIPTION_LENGTH === true &&
$attribute === TARGET_DESCRIPTION_ATTRIBUTE &&
strlen($value) < MIN_TARGET_DESCRIPTION_LENGTH)
{
$targetsWithTooShortDescription[] = $actualTargetName;
}
}
if (!in_array(TARGET_DESCRIPTION_ATTRIBUTE, $allUsedTragetAttributes)) {
$targetsWithNoDescription[] = $actualTargetName;
}
}
if (count($targetsWithNoDescription) > 0) {
if (count($targetsWithNoDescription) > 1) {
$violations[] = sprintf(
"Build targets [ %s ] don't have mandatory descriptions.",
implode(', ', $targetsWithNoDescription)
);
} else {
$violations[] = sprintf(
"Build target [ %s ] doesn't have a mandatory description.",
implode(', ', $targetsWithNoDescription)
);
}
}
if (count($targetsWithTooShortDescription) > 0) {
if (count($targetsWithTooShortDescription) > 1) {
$violations[] = sprintf(
"Build targets [ %s ] don't have an adequate target description length.",
implode(', ', $targetsWithTooShortDescription),
MIN_TARGET_DESCRIPTION_LENGTH
);
} else {
$violations[] = sprintf(
"Build target [ %s ] doesn't have an adequate target description length.",
implode(', ', $targetsWithTooShortDescription),
MIN_TARGET_DESCRIPTION_LENGTH
);
}
}
}
return $violations;
}</pre><a href="http://www.flickr.com/photos/raphaelstolt/5643107165/" title="Non-Descriptive Phing build files rejected by a Git hook von Raphael Stolt bei Flickr"><img alt="Non-Descriptive Phing build files rejected by a Git hook" border="0" height="147" src="http://farm6.static.flickr.com/5046/5643107165_4e6a34bfe9.jpg" width="500" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-58522892396983861072010-11-20T16:30:00.003+01:002011-11-09T12:50:09.767+01:00Measuring & displaying Phing build times with buildhawkRecently I installed a Ruby gem called <a href="http://github.com/xaviershay/buildhawk">buildhawk</a> which allows to measure and display the build times of <a href="http://rake.rubyforge.org/">Rake</a> driven builds. As I like the idea behind this tool a lot but mostly use <a href="http://phing.info/">Phing</a> for build orchestration, it was time to explore the possibility to interconnect them both. In this blog post I'll show an implementation of an apposite Phing Logger gathering the buildhawk compatible build times via <a href="http://progit.org/2010/08/25/notes.html">git note</a>(s) and how to put the interplay between those two tools to work.<br />
<h4 class="custom">Logging on</h4>As mentioned above the build time of each build is stored as a git note and associated to the repository's HEAD, reflecting the current state of the system under build (SUB), which assumes that the SUB is versioned via Git. The next shown Phing Logger (i.e. BuildhawkLogger) grabs the overall build time by hooking into the <i>buildFinished</i> method of the extended <i>DefaultLogger</i> class, transforms it into a buildhawk specific format and finally adds it as a git note.<br />
<pre class="codeSnippet"><?php
require_once 'phing/listener/DefaultLogger.php';
/**
* Writes a build event to the console and store the build time as a git notes in the
* project's repository HEAD.
*
* @author Raphael Stolt <raphael.stolt@gmail.com>
* @see BuildEvent
* @link https://github.com/xaviershay/buildhawk Buildhawk on GitHub
* @package phing.listener
*/
class BuildhawkLogger extends DefaultLogger {
/**
* @var string
*/
private $_gitNotesCommandResponse = null;
/**
* Behaves like the original DefaultLogger, plus adds the total build time
* as a git note to current repository HEAD.
*
* @param BuildEvent $event
* @see BuildEvent::getException()
* @see DefaultLogger::buildFinished
* @link http://www.kernel.org/pub/software/scm/git/docs/git-notes.html
*/
public function buildFinished(BuildEvent $event) {
parent::buildFinished($event);
if ($this->_isProjectGitDriven($event)) {
$error = $event->getException();
if ($error === null) {
$buildtimeForBuildhawk = $this->_formatBuildhawkTime(
Phing::currentTimeMillis() - $this->startTime
);
if (!$this->_addBuildTimeAsGitNote($buildtimeForBuildhawk)) {
$message = sprintf(
"Failed to add git note due to '%s'",
$this->_gitNotesCommandResponse
);
$this->printMessage($message, $this->err, Project::MSG_ERR);
}
}
}
}
/**
* Checks (rudimentary) if the project is Git driven
*
* @param BuildEvent $event
* @return boolean
*/
private function _isProjectGitDriven(BuildEvent $event)
{
$project = $event->getProject();
$projectRelativeGitDir = sprintf(
'%s/.git', $project->getBasedir()->getPath()
);
return file_exists($projectRelativeGitDir) && is_dir($projectRelativeGitDir);
}
/**
* Formats a time micro integer to buildhawk readable format.
*
* @param integer The time stamp
*/
private function _formatBuildhawkTime($micros) {
return sprintf("%0.3f", $micros);
}
/**
* Adds the build time as a git note to the current repository HEAD
*
* @param string $buildTime The build time of the build
* @return mixed True on sucess otherwise the command failure response
*/
private function _addBuildTimeAsGitNote($buildTime) {
$gitNotesCommand = sprintf(
"git notes --ref=buildtime add -f -m '%s' HEAD 2>&1",
$buildTime
);
$gitNotesCommandResponse = exec($gitNotesCommand, $output, $return);
if ($return !== 0) {
$this->_gitNotesCommandResponse = $gitNotesCommandResponse;
return false;
}
return true;
}
}</pre><h4 class="custom">Putting the Logger to work</h4>As the buildhawk logger is <a href="https://gist.github.com/707868">available</a> via GitHub you can easily grab it by issuing <i>sudo curl -s http://gist.github.com/raw/707868/BuildhawkLogger.php -o $PHING_HOME/listener/BuildhawkLogger.php</i>. The next step, making the build times loggable, is achieved by using the <i>-logger</i> command line argument of the Phing Cli and specifying the buildhawk logger name or the path to it. In case you want the buildhawk logger to be used per default (it behaves like the default logger if the SUB isn't Git driven/managed) you can also add it to the Phing shell script.<br />
<br />
The next console command issued in the directory of the SUB shows a Phing call utilizing the BuildhawkLogger, assumed it has been installed at <i>$PHING_HOME/listener/BuildhawkLogger.php</i> and not been made the default logger.<br />
<pre class="consoleOutput">phing -logger phing.listener.BuildhawkLogger</pre><h4 class="custom">Looking at them Phing build times</h4>Now it's time to switch to buildhawk and let it finally perform it's designated task, rendering an with the commit SHAs, commit messages, and build times fed <a href="http://ruby-doc.org/stdlib/libdoc/erb/rdoc/classes/ERB.html">Erb</a> template into an informative, viewable HTML page. To install it you simply have to run <i>sudo gem install buildhawk</i> and you're good to go.<br />
<br />
The next console command shows the buildhawk call issued in the SUB's directory to produce it's build time report page.<br />
<pre class="consoleOutput">buildhawk --title 'Examplr' > examplr-build-times.html</pre>The outro screenshot below gives you a peek at a rendered build time report.<a href="http://www.flickr.com/photos/raphaelstolt/5192234632/" title="Buildhawk report for a Phing driven build von Raphael Stolt bei Flickr"><img alt="Buildhawk report for a Phing driven build" border="0" height="270" src="http://farm5.static.flickr.com/4091/5192234632_736ff764e9.jpg" width="500" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-19003910051625469062010-06-03T05:05:00.005+02:002010-11-18T23:11:08.790+01:00Growling PHPUnit's test status<a href="http://www.flickr.com/photos/raphaelstolt/4428732239/" title="PHPUnit Growl TestListener by Raphael Stolt, on Flickr"><img alt="PHPUnit Growl TestListener" border="0" src="http://farm5.static.flickr.com/4022/4428732239_b35eefcbf0_o.gif" style="cursor: pointer; float: left; height: 110px; margin: 0pt 10px 10px 0pt; width: 268px;" title="PHPUnit Growl TestListener" /></a>Two years ago I <a href="http://raphaelstolt.blogspot.com/2008/04/hooking-growl-publisher-plugin-into.html" target="_self">blogged</a> about a <a href="http://code.google.com/p/xinc/" target="_self">Xinc</a> (R.I.P?) plugin that <a href="http://growl.info/" target="_self">growls</a> each build status for any via Xinc continuously integrated project. Since I'm using <a href="http://www.phpunit.de/" target="_self">PHPUnit</a> more and more lately, especially in continuous testing sessions (sprints without hitting the continuous integration server), my dependence on a fast and more visual feedback loop rose. In this post I'll provide an easy solution that meets these requirements by utilizing PHPUnit's test listener feature.<br />
<br />
<h4 class="custom">What's the motivation, yo?</h4>While doing story or feature sprints embedded in a continuous testing approach I first used a combination of <a href="http://gist.github.com/159470" target="_self">stakeout.rb</a> and PHPUnit's --colors option to radiate the tests status, but soon wasn't that satisfied with the chosen route as it happened that the console window got superimposed with other opened windows (e.g. API Browser, TextMate etc.) especially on my 13,3" MacBook. <br />
<br />
To overcome this misery I decided to utilize PHPUnit's ability to write custom test listeners and to implement one that radiates the test status in a more prominent and sticky spot via Growl.<br />
<h4 class="custom">Implementing the Growl test listener</h4>Similar to the ticket listener plugin mechanism I blogged about <a href="http://raphaelstolt.blogspot.com/2010/01/closing-and-reopening-github-issues-via.html" target="_self">earlier</a> PHPUnit also provides one for test listeners. This extension mechanism allows to bend the test result formatting and output to the given needs and scenarios a developer might face and therefore is a perfect match. <br />
<br />
To customize the test feedback and visualization the test listener has to implement the provided PHPUnit_Framework_Testlistener interface. A few keystrokes later I ended up with the next shown implementation, which is also available via a GitHub <a href="http://gist.github.com/241303" target="_self">gist</a>, supporting the previous stated requirements.<br />
<pre class="codeSnippet"><?php
class PHPUnit_Extensions_TestListener_GrowlTestListener
implements PHPUnit_Framework_Testlistener
{
const TEST_RESULT_COLOR_RED = 'red';
const TEST_RESULT_COLOR_YELLOW = 'yellow';
const TEST_RESULT_COLOR_GREEN = 'green';
private $_errors = array();
private $_failures = array();
private $_incompletes = array();
private $_skips = array();
private $_tests = array();
private $_suites = array();
private $_endedSuites = 0;
private $_assertionCount = 0;
private $_startTime = 0;
private $_successPicturePath = null;
private $_incompletePicturePath = null;
private $_failurePicturePath = null;
/**
* @param string $successPicturePath
* @param string $incompletePicturePath
* @param string $failurePicturePath
*/
public function __construct($successPicturePath, $incompletePicturePath,
$failurePicturePath)
{
$this->_successPicturePath = $successPicturePath;
$this->_incompletePicturePath = $incompletePicturePath;
$this->_failurePicturePath = $failurePicturePath;
}
public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
{
$this->_errors[] = $test->getName();
}
public function addFailure(PHPUnit_Framework_Test $test,
PHPUnit_Framework_AssertionFailedError $e, $time)
{
$this->_failures[] = $test->getName();
}
public function addIncompleteTest(PHPUnit_Framework_Test $test,
Exception $e, $time)
{
$this->_incompletes[] = $test->getName();
}
public function addSkippedTest(PHPUnit_Framework_Test $test,
Exception $e, $time)
{
$this->_skips[] = $test->getName();
}
public function startTest(PHPUnit_Framework_Test $test)
{
}
public function endTest(PHPUnit_Framework_Test $test, $time)
{
$this->_tests[] = array('name' => $test->getName(),
'assertions' => $test->getNumAssertions()
);
$this->_assertionCount+= $test->getNumAssertions();
}
public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
if (count($this->_suites) === 0) {
PHP_Timer::start();
}
$this->_suites[] = $suite->getName();
}
public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
$this->_endedSuites++;
if (count($this->_suites) <= $this->_endedSuites)
{
$testTime = PHP_Timer::secondsToTimeString(
PHP_Timer::stop());
if ($this->_isGreenTestResult()) {
$resultColor = self::TEST_RESULT_COLOR_GREEN;
}
if ($this->_isRedTestResult()) {
$resultColor = self::TEST_RESULT_COLOR_RED;
}
if ($this->_isYellowTestResult()) {
$resultColor = self::TEST_RESULT_COLOR_YELLOW;
}
$suiteCount = count($this->_suites);
$testCount = count($this->_tests);
$failureCount = count($this->_failures);
$errorCount = count($this->_errors);
$incompleteCount = count($this->_incompletes);
$skipCount = count($this->_skips);
$resultMessage = '';
if ($suiteCount > 1) {
$resultMessage.= "Suites: {$suiteCount}, ";
}
$resultMessage.= "Tests: {$testCount}, ";
$resultMessage.= "Assertions: {$this->_assertionCount}";
if ($failureCount > 0) {
$resultMessage.= ", Failures: {$failureCount}";
}
if ($errorCount > 0) {
$resultMessage.= ", Errors: {$errorCount}";
}
if ($incompleteCount > 0) {
$resultMessage.= ", Incompletes: {$incompleteCount}";
}
if ($skipCount > 0) {
$resultMessage.= ", Skips: {$skipCount}";
}
$resultMessage.= " in {$testTime}.";
$this->_growlnotify($resultColor, $resultMessage);
}
}
/**
* @param string $resultColor
* @param string $message
* @param string $sender The name of the application that sends the notification
* @throws RuntimeException When growlnotify is not available
*/
private function _growlnotify($resultColor, $message = null, $sender = 'PHPUnit')
{
if ($this->_isGrowlnotifyAvailable() === false) {
throw new RuntimeException('The growlnotify tool is not available');
}
$notificationImage = $this->_getNotificationImageByResultColor(
$resultColor);
$command = "growlnotify -w -s -m '{$message}' "
. "-n '{$sender}' "
. "-p 2 --image {$notificationImage}";
exec($command, $response, $return);
}
/**
* @return boolean
*/
private function _isGrowlnotifyAvailable()
{
exec('growlnotify -v', $reponse, $status);
return ($status === 0);
}
/**
* @param string $color
* @return string
*/
private function _getNotificationImageByResultColor($color)
{
switch ($color) {
case self::TEST_RESULT_COLOR_RED:
return $this->_failurePicturePath;
break;
case self::TEST_RESULT_COLOR_GREEN:
return $this->_successPicturePath;
break;
default:
return $this->_incompletePicturePath;
}
}
/**
* @return boolean
*/
private function _isGreenTestResult()
{
return count($this->_errors) === 0 &&
count($this->_failures) === 0 &&
count($this->_incompletes) === 0 &&
count($this->_skips) === 0;
}
/**
* @return boolean
*/
private function _isRedTestResult()
{
return count($this->_errors) > 0 ||
count($this->_failures) > 0;
}
/**
* @return boolean
*/
private function _isYellowTestResult()
{
return count($this->_errors) === 0 &&
count($this->_failures) === 0 &&
(count($this->_incompletes) > 0 ||
count($this->_skips) > 0);
}
}</pre><h4 class="custom">Hooking the Growl test listener into the PHPUnit ecosystem</h4>To make use of the just outlined test listener it's necessary to add an entry to PHPUnit's <a href="http://www.phpunit.de/manual/current/en/appendixes.configuration.html" target="_self">XML configuration file</a> telling PHPUnit which test listener class to utilize and where it's located in the file system. In a next step the <a href="http://thelucid.com/files/autotest_images.zip" target="_parent">images</a> for the three possible Growl notifications have to be added to the local file system, and as the Growl test listener constructor takes these as arguments they have also to be injected in the PHPUnit XML configuration file (i.e. <i>phpunit-offline.xml</i>). Take a peek yourself how this is done in the next listing.<br />
<pre class="xmlSnippet"><phpunit backupGlobals="false"
backupStaticAttributes="true"
bootstrap="bootstrap.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="true"
stopOnFailure="true"
syntaxCheck="true"
testSuiteLoaderClass="PHPUnit_Runner_StandardTestSuiteLoader">
<testsuites>
<testsuite name="Zend_Service_GitHub Offline Testsuite">
<directory>Zend/Service/GitHub</directory>
<directory>Zend/Service</directory>
</testsuite>
</testsuites>
<groups>
<include>
<group>offline</group>
</include>
</groups>
<listeners>
<listener class="PHPUnit_Extensions_TestListener_GrowlTestListener"
file="/Users/stolt/Work/GrowlTestListener.php">
<arguments>
<string>$HOME/Pictures/pass.png</string>
<string>$HOME/Pictures/pending.png</string>
<string>$HOME/Pictures/fail.png</string>
</arguments>
</listener>
</listeners>
</phpunit></pre><h4 class="custom">Putting the Growl test listener to work</h4><i>Attention shameless plug!</i> As an example application for a continuous testing session I chose a <a href="http://framework.zend.com/" target="_self">Zend Framework</a> Service component I'm currently working on. To set up the continuously testing workflow, stakeout.rb is still my #1 choice, but in a recent blog <a href="http://www.negativemargins.com/2010/05/02/automaticlly-run-phpunit-tests/" target="_self">post</a> Andy Stanberry shows another tool dubbed <a href="http://github.com/alloy/kicker" target="_self">Kicker</a> which seems to be coequal. The following console snippet shows in a concrete scenario how to utilize stakeout.rb to watch for any changes on the <a href="http://github.com/raphaelstolt/github-api-client" target="_self">Zend_Service_GitHub</a> component or it's backing tests which immediately trigger the test suite execution if one is detected.<br />
<pre class="consoleOutput">stakeout.rb 'phpunit --configuration phpunit-offline.xml' **/*.{php} ../Zend/**/*.php ../Zend/Service/**/*.php
</pre>In the classic TDD cycle we start with a failing test. Creating the test, adding the assertions that the system under test (SUT) has to fulfill and saving the according test class automatically triggers the test suite execution which ends up in the next shown Growl notification.<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/4665080708/" title="Growl notice for failed tests by Raphael Stolt, on Flickr"><img alt="Growl notice for failed tests" border="0" height="95" src="http://farm2.static.flickr.com/1283/4665080708_dba552b811_b.jpg" width="510" /></a><br />
<br />
Nest a très important client call comes in and since we are clever, a quick TextMate shortcut marks the currently worked on test as incomplete. This step <i>might</i> be a bit controversy as it's also suggested to leave the last worked on test broken, but I got to show you the pending/incomplete Growl notification ;D<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/4665080614/" title="Growl notice for incomplete tests by Raphael Stolt, on Flickr"><img alt="Growl notice for incomplete tests" border="0" height="95" src="http://farm5.static.flickr.com/4002/4665080614_661b6ede54_b.jpg" width="510" /></a><br />
<br />
After finishing the 'interruptive' client call aka context switch we can continue to work on the feature of the SUT until it fulfills the expected behavior which will be radiated via the next shown Growl notification. Happy Growl flavored testing!<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/4665080660/" title="Growl notice for successful tests by Raphael Stolt, on Flickr"><img alt="Growl notice for successful tests" border="0" height="95" src="http://farm2.static.flickr.com/1297/4665080660_6e28e651bf_b.jpg" width="510" /></a><br />
<br />
<i>* As you might notice in the shown Growl notification images there's a test suite count of 9 while we are only operating on a single one, this seems to be a possible PHPUnit <a href="http://www.phpunit.de/ticket/1061" target="_self">bug</a>, or just a misconfiguration of my testing environment. <br />
<br />
In case you got a solution for this problem feel free to add an illuminating comment.</i>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com1tag:blogger.com,1999:blog-8420118650236071171.post-75602730287068224942010-05-15T07:53:00.002+02:002015-11-16T14:43:39.513+01:00Installing the PHP redis extension on Mac OS XRecently I took a look at <a href="http://code.google.com/p/redis/" target="_self">Redis</a>, a popular and advanced key-value store. Peeking at the <a href="http://code.google.com/p/redis/#Supported_languages" target="_self">supported languages</a> section of the project's website you'll notice a lot of client libraries available for PHP. Two out of them caught my particular attention: <a href="http://rediska.geometria-lab.net/" target="_self">Rediska</a> due to it's impressive <a href="http://framework.zend.com" target="_self">Zend Framework</a> integration and <a href="https://github.com/nicolasff/phpredis" target="_self">phpredis</a> as it's a native PHP extension written in C and therefore supposed to be blazingly faster than vanilla PHP client libraries. The following blog post will show how to install and configure the aforementioned, native PHP extension on a Mac OS X system.<br />
<br />
The next steps assume that you've installed redis on your machine. In case you are using <a href="http://www.macports.org" target="_self">MacPorts</a> and haven't installed the key-value store yet, all it takes are the following two commands and you're good to go. In case you prefer <a href="http://mxcl.github.com/homebrew/" target="_self">Homebrew</a> for managing your package/software installations, there's also a Formula for redis <a href="http://github.com/mxcl/homebrew/blob/master/Library/Formula/redis.rb" target="_self">available</a> that allows you to install it via <em>brew install redis</em>.<pre class="consoleOutput">sudo port install redis
sudo launchctl load -w /Library/LaunchDaemons/org.macports.redis.plist</pre>The very first step for building the native PHP redis extension is to get the source code by cloning the GitHub <a href="http://github.com/owlient/phpredis/tree/master" target="_self">repository</a> of the extension without it's history revisions.<pre class="consoleOutput">mkdir phpredis-build
cd phpredis-build
git clone --depth 1 git://github.com/nicolasff/phpredis.git
cd phpredis</pre>The next task is to compile the extension with the following batch of commands.<pre class="consoleOutput">phpize
./configure
make
sudo make install</pre>The next to last step is to alternate your php.ini, use <em>php --ini | grep 'Loaded'</em> to get the location of it on your system, so that the redis module/extension is available to your PHP ecosystem. Therefor simply add <em>extension=redis.so</em> in the <em>Dynamic Extensions</em> section of your php.ini. Afterwards you can verify that the redis module is loaded and available via one of the following commands.<pre class="consoleOutput">php -m | grep redis
php -i | grep 'Redis Support'</pre>To make the extension also available to the running Apache PHP module you'll need to restart the Apache server. Looking at <em>phpinfo()</em>'s output in a browser you should see the entry shown in the next image.<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/4607738861/" title="Enabled redis extension by Raphael Stolt, on Flickr"><img src="http://farm4.static.flickr.com/3320/4607738861_d24901cd7d_o.gif" width="608" height="199" border="0" alt="Enabled redis extension" /></a><br />
<br />
For testing the communication between the just installed redis extension and the running Redis server, I further created a simple test script called <a href="http://gist.github.com/402018" target="_self">redis-glue-test.php</a> you can fetch from GitHub and run via the next commands.<pre class="consoleOutput">curl -s http://gist.github.com/raw/402018/redis-glue-test.php -o redis-glue-test.php
php redis-glue-test.php</pre>When you see the following shown console output you're good to go. Happy Redising!<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/4608312060/" title="Output of redis-glue-test.php by Raphael Stolt, on Flickr"><img src="http://farm2.static.flickr.com/1295/4608312060_9de6daae3f_o.gif" width="627" height="304" border="0" alt="Output of redis-glue-test.php" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com7tag:blogger.com,1999:blog-8420118650236071171.post-60410054837520874072010-03-16T22:32:00.009+01:002010-06-24T18:17:17.906+02:00Using MongoHq in Zend Framework based applications<a href="http://www.flickr.com/photos/raphaelstolt/4429498884/" title="MongoHq logo"><img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 140px; height: 150px;" src="http://farm5.static.flickr.com/4056/4429498884_d39554767c_o.gif" alt="MongoHq logo" title="MongoHq logo" border="0" /></a>As the name slightly foreshadows <a href="https://app.mongohq.com/home" target="_self">MongoHq</a> is a <em>currently</em> bit pricey cloud-based hosting solution for <a href="http://www.mongodb.org" target="_self">MongoDb</a> databases provided by <a href="http://www.commonthread.com" target="_self">CommonThread</a>. Since they went live a few weeks ago I signed up for the small plan and started to successfully re-thinker with it in an exploratory <a href="http://framework.zend.com" target="_self">Zend Framework</a> based application. <br /><br />Therefore the following post will show how to bootstrap such an instance into a Zend Framework based application and how to use it from there in some simple scenarios like storing data coming from a Zend_Form into a designated collection and vice versa fetching it from there.<br /><br /><h4 class="custom">Bootstrapping a MongoHq enabled connection</h4>To establish and make the MongoDb connection application-wide available the almighty <a href="http://framework.zend.com/manual/en/zend.application.html" target="_self">Zend_Application</a> component came to the rescue again. After reading Matthew Weier O'Phinney's enlightening blog <a href="http://weierophinney.net/matthew/archives/231-Creating-Re-Usable-Zend_Application-Resource-Plugins.html" target="_self">post</a> about creating re-usable Zend_Application resource plugins and deciding to use MongoDb in some more exploratory projects, I figured it would be best to create such a plugin and ditch the also possible resource method approach. <br /><br />The next code listing shows a possible implementation of the MongoDb resource plugin initializing a <a href="http://php.net/manual/en/class.mongo.php" target="_self">Mongo</a> instance for the given <em>APPLICATION_ENV</em> (i.e. production) mode. <br /><br />For the other application environment modes (development | testing | staging) it's currently assumed that no database authentication is enabled, which is also the default when using MongoDb, so you might need to adapt the plugin to your differing needs; and since I'm currently only rolling on the <a href="https://app.mongohq.com/signup" target="_self">small</a> plan the support for multiple databases is also not accounted for.<br /><br /><div class="refactoringStatus" style="width: 265px;">library/Recordshelf/Resource/MongoDb.php</div><pre class="codeSnippetRefactoring"><?php<br /><br />class Recordshelf_Resource_MongoDb <br /> extends Zend_Application_Resource_ResourceAbstract<br />{<br /> /**<br /> * Definable Mongo options.<br /> *<br /> * @var array<br /> */<br /> protected $_options = array(<br /> 'hostname' => '127.0.0.1',<br /> 'port' => '27017',<br /> 'username' => null,<br /> 'password' => null,<br /> 'databasename' => null,<br /> 'connect' => true<br /> );<br /> /**<br /> * Initalizes a Mongo instance.<br /> *<br /> * @return Mongo<br /> * @throws Zend_Exception<br /> */<br /> public function init()<br /> {<br /> $options = $this->getOptions();<br /><br /> if (null !== $options['username'] && <br /> null !== $options['password'] &&<br /> null !== $options['databasename'] &&<br /> 'production' === APPLICATION_ENV) {<br /> // Database Dns with MongoHq credentials<br /> $mongoDns = sprintf('mongodb://%s:%s@%s:%s/%s',<br /> $options['username'],<br /> $options['password'],<br /> $options['hostname'],<br /> $options['port'],<br /> $options['databasename']<br /> );<br /> } elseif ('production' !== APPLICATION_ENV) {<br /> $mongoDns = sprintf('mongodb://%s:%s/%s',<br /> $options['hostname'],<br /> $options['port'],<br /> $options['databasename']<br /> );<br /> } else {<br /> $exceptionMessage = sprintf(<br /> 'Recource %s is not configured correctly',<br /> __CLASS__<br /> );<br /> throw new Zend_Exception($exceptionMessage);<br /> }<br /> try {<br /> return new Mongo($mongoDns, array('connect' => $options['connect']));<br /> } catch (MongoConnectionException $e) {<br /> throw new Zend_Exception($e->getMessage());<br /> }<br /> }<br />}</pre>With the MongoDb resource plugin in the place to be, it's time to make it known to the boostrapping mechanism which is done by registering the resource plugin in the application.ini. <br /><br />Further the MongoHq credentials, which are available in the <em>MongoHq > My Database</em> section, and the main database name are added to the configuration file which will be used to set the definable resource plugin ($_)options and to connect to the hosted database.<br /><br /><div class="refactoringStatus" style="width: 205px;">application/configs/application.ini</div><pre class="codeSnippetRefactoring"><br />[production]<br />pluginPaths.Recordshelf_Resource = "Recordshelf/Resource"<br />resources.mongodb.username = __MONGOHQ_USERNAME__<br />resources.mongodb.password = __MONGOHQ_PASSWORD__<br />resources.mongodb.hostname = __MONGOHQ_HOSTNAME__<br />resources.mongodb.port = __MONGOHQ_PORT__<br />resources.mongodb.databasename = __MONGOHQ_DATABASENAME__<br /><br />...<br /></pre><h4 class="custom">Cloudifying documents into collections</h4>Having the MongoHq enabled connection in the bootstrapping mechanism it can now be picked up from there and used in any Zend Framework application context. <br /><br />The example action method (i.e. proposeAction) assumes data (i.e. a tech talk proposal to revive the <strong>example</strong> domain from my <a href="http://raphaelstolt.blogspot.com/2010/02/utilizing-twitter-lists-with.html" target="_self">last</a> blog post) coming from a <a href="http://framework.zend.com/manual/en/zend.form.html" target="_self">Zend_Form</a> which will be stored in a collection named <em>proposals</em>, a table in <em>old</em> relational database think. <br /><br />The next code listings states the action method innards to do so by injecting the valid form values into a model class which provides accessors and mutators for the domain model's properties and can transform them into a proposal document aka an array structure.<br /><br /><div class="refactoringStatus" style="width: 275px;">application/controllers/ProposalController.php</div><pre class="codeSnippetRefactoring"><?php<br /><br />class ProposalController extends Zend_Controller_Action<br />{<br /> public function indexAction()<br /> {<br /> $this->view->form = new Recordshelf_Form_Proposal();<br /> }<br /> public function thanksAction()<br /> {<br /> }<br /> public function proposeAction()<br /> {<br /> $this->_helper->viewRenderer->setNoRender();<br /> $form = new Recordshelf_Form_Proposal();<br /> <br /> $request = $this->getRequest();<br /> <br /> if ($this->getRequest()->isPost()) {<br /> if ($form->isValid($request->getPost())) {<br /> $model = new Recordshelf_Model_Proposal($form->getValues());<br /> $mapper = new Recordshelf_Model_ProposalMapper();<br /> if ($mapper->insert($model)) {<br /> return $this->_helper->redirector('thanks');<br /> }<br /> $this->view->form = $form;<br /> return $this->render('index');<br /> } else {<br /> $this->view->form = $form;<br /> return $this->render('index');<br /> }<br /> }<br /> }<br />}</pre>Next the model/data mappper is initialized, which triggers the picking of the MongoHq enabled Mongo connection instance and the auto-determination of the collection name to use based on the mapper's class name. Subsequently the populated model instance is passed into the mappper's <em>insert</em> method which is pulling the document (array structure) and doing the actual insert into the <em>proposals</em> collection. <br /><br />To give you an idea of the actual document structure it's shown in the next listing, followed by the model/data mapper implementation.<pre class="codeSnippet">Array<br />(<br /> [state] => new<br /> [created] => MongoDate Object<br /> (<br /> [sec] => 1268774242<br /> [usec] => 360831<br /> )<br /><br /> [submitee] => Array<br /> (<br /> [title] => Mr<br /> [firstname] => John<br /> [familyname] => Doe<br /> [email] => john.doe@gmail.com<br /> [twitter] => johndoe<br /> )<br /><br /> [title] => How to get a real name<br /> [description] => Some descriptive text...<br /> [topictags] => Array<br /> (<br /> [0] => John<br /> [1] => Doe<br /> [2] => Anonymous<br /> )<br /><br />)</pre><br /><br /><div class="refactoringStatus" style="width: 240px;">application/models/ProposalMapper.php</div><pre class="codeSnippetRefactoring"><?php<br /><br />class Recordshelf_Model_ProposalMapper<br />{<br /> private $_mongo;<br /> private $_collection;<br /> private $_databaseName;<br /> private $_collectionName;<br /><br /> public function __construct()<br /> {<br /> $frontController = Zend_Controller_Front::getInstance();<br /> $this->_mongo = $frontController->getParam('bootstrap')<br /> ->getResource('mongoDb');<br /> $config = $frontController->getParam('bootstrap')<br /> ->getResource('config');<br /> <br /> $this->_databaseName = $config->resources->mongodb->get('databasename');<br /> <br /> $replaceableClassNameparts = array(<br /> 'recordshelf_model_', <br /> 'mapper'<br /> ); <br /> $this->_collectionName = str_replace($replaceableClassNameparts, '',<br /> strtolower(__CLASS__) . 's');<br /><br /> $this->_collection = $this->_mongo->selectCollection(<br /> $this->_databaseName, <br /> $this->_collectionName<br /> );<br /> }<br /> /**<br /> * Inserts a proposal document/model into the proposals collection.<br /> *<br /> * @param Recordshelf_Model_Proposal $proposal The proposal document/model.<br /> * @return MongoId<br /> * @throws Zend_Exception<br /> */<br /> public function insert(Recordshelf_Model_Proposal $proposal)<br /> {<br /> $proposalDocument = $proposal->getValues();<br /> try {<br /> if ($this->_collection->insert($proposalDocument, true)) {<br /> return $proposalDocument['_id'];<br /> }<br /> } catch (MongoCursorException $mce) {<br /> throw new Zend_Exception($mce->getMessage());<br /> }<br /> }<br />}</pre><h4 class="custom">Querying and retrieving the cloudified data</h4>As what comes in must come out, the next interaction with the Document Database Management System (DocDBMS) is about retrieving some afore-stored talk proposal documents from the collection so they can be rendered to the application's user. This isn't really MongoHq specific anymore, like most of the previous model parts, and is just here to round up this blog post and use some more of that MongoDb goodness. <em>Looks like I have to look for an anonymous self-help group that stuff is highly addictive.</em> <br /><br />Anyway the next listing shows the action method fetching all stored documents available in the <em>proposals</em> collection. To save some CO2 on this blog post all documents are fetched, which ends up in the most trivial query but as you can figure the example domain provides a bunch of query examples like only proposals for a given topic tag, specific talk title or a given proposal state which can be easily created via passed-through Http request parameters.<br /><br /><div class="refactoringStatus" style="width: 275px;">application/controllers/ProposalController.php</div><pre class="codeSnippetRefactoring"><?php<br /><br />class ProposalController extends Zend_Controller_Action<br />{<br /> ...<br /> <br /> public function listAction()<br /> {<br /> $mapper = new Recordshelf_Model_ProposalMapper();<br /> $proposals = $mapper->fetchAll();<br /> // For iterating the Recordshelf_Model_Proposal's in the view<br /> $this->view->proposals = $proposals;<br /> }<br />}</pre>The last code listing shows the above used <em>fetchAll</em> method of the data mapper class returning an array of stored proposal documents mapped to their domain model (i.e. Recordshelf_Model_Proposal) in the application.<br /><br /><div class="refactoringStatus" style="width: 240px;">application/models/ProposalMapper.php</div><pre class="codeSnippetRefactoring"><?php<br /><br />class Recordshelf_Model_ProposalMapper<br />{<br /> ...<br /> <br /> /**<br /> * Fetches all stored talk proposals.<br /> *<br /> * @return array<br /> */<br /> public function fetchAll()<br /> {<br /> $cursor = $this->_collection->find();<br /> $proposals = array();<br /> <br /> foreach ($cursor as $documents) {<br /> $proposal = new Recordshelf_Model_Proposal();<br /> foreach ($documents as $property => $value) {<br /> if ('submitee' === $property) {<br /> $proposal->submitee = new Recordshelf_Model_Submitee($value);<br /> } else {<br /> $proposal->$property = $value;<br /> }<br /> }<br /> $proposals[] = $proposal;<br /> }<br /> return $proposals;<br /> }<br />}</pre>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com3tag:blogger.com,1999:blog-8420118650236071171.post-40280082040919031752010-02-05T02:05:00.002+01:002010-02-05T02:15:10.706+01:00Utilizing Twitter lists with Zend_Service_Twitter<img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 280px; height: 176px;" src="http://farm3.static.flickr.com/2727/4329909679_49489b5345_o.gif" alt="Twitter lists with the Zend Framework" border="0" />Several months ago <a href="http://twitter.com/" target="_self">Twitter</a> added the list feature to it's public <a href="http://apiwiki.twitter.com/Twitter-API-Documentation" target="_self">API</a>. While debating some use cases for an event registration application I stumbled upon an interesting feature, which adds participants automatically to a Twitter list upon registration. This way registered and interested users can discover like-minded individuals and get in touch prior to any pre-social event activities. This post will show how this feature can be implemented by utilizing the <a href="http://framework.zend.com/manual/en/zend.service.twitter.html" target="_self">Zend_Service_Twitter</a> component, and how it then can be used in a <a href="http://framework.zend.com" target="_self">Zend Framework</a> based application.<h4 class="custom">Implementing the common list features</h4>Looking at the three relevant parts of the Twitter list API some common features emerged and had to be supported to get the feature out of the door. These are namely the creation, deletion of new lists and the addition, removal of list members (i.e. event participants). Since the current Twitter component doesn't support these list operations out of the box it was time to put that develeoper hat on and get loose; which was actually a joy due to the elegance of the extended Zend_Service_Twitter component laying all the groundwork. <br /><br />A non-feature-complete implementation is shown in the next code listing and can alternatively be <a href="http://github.com/raphaelstolt/zf-hacks/blob/master/Recordshelf/Service/Twitter/List.php" target="_self">pulled</a> from <a href="http://github.com/" target="_self">GitHub</a>. Currently it only supports the above stated common operations plus the ability to get the lists of a Twitter account and it's associated members; but feel free to fork it or even turn it into an official proposal.<pre class="codeSnippet"><?php<br /><br />require_once 'Zend/Service/Twitter.php';<br />require_once 'Zend/Service/Twitter/Exception.php';<br /><br />class Recordshelf_Service_Twitter_List extends Zend_Service_Twitter<br />{<br /> const LIST_MEMBER_LIMIT = 500;<br /> const MAX_LIST_NAME_LENGTH = 25;<br /> const MAX_LIST_DESCRIPTION_LENGTH = 100;<br /> <br /> /**<br /> * Initializes the service and adds the list to the method types <br /> * of the parent service class.<br /> *<br /> * @param string $username The Twitter account name.<br /> * @param string $password The Twitter account password.<br /> * @see Zend_Service_Twitter::_methodTypes<br /> */<br /> public function __construct($username = null, $password = null)<br /> {<br /> parent::__construct($username, $password);<br /> $this->_methodTypes[] = 'list';<br /> }<br /> /**<br /> * Creates a list associated to the current user.<br /> *<br /> * @param string $listname The listname to create.<br /> * @param array $options The options to set whilst creating the list. <br /> * Allows to set the list creation mode (public|private) <br /> * and the list description.<br /> * @return Zend_Rest_Client_Result<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> public function create($listname, array $options = array())<br /> {<br /> $this->_init();<br /> <br /> if ($this->_existsListAlready($listname)) {<br /> $exceptionMessage = 'List with name %s exists already';<br /> $exceptionMessage = sprintf($exceptionMessage, $listname);<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> <br /> $_options = array('name' => $this->_validListname($listname));<br /> foreach ($options as $key => $value) {<br /> switch (strtolower($key)) {<br /> case 'mode':<br /> $_options['mode'] = $this->_validMode($value);<br /> break;<br /> case 'description':<br /> $_options['description'] = $this->_validDescription($value);<br /> break;<br /> default:<br /> break;<br /> }<br /> }<br /> $path = '/1/%s/lists.xml';<br /> $path = sprintf($path, $this->getUsername());<br /> <br /> $response = $this->_post($path, $_options);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Deletes an owned list of the current user.<br /> *<br /> * @param string $listname The listname to delete.<br /> * @return Zend_Rest_Client_Result<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> public function delete($listname)<br /> {<br /> $this->_init();<br /> <br /> if (!$this->_isListAssociatedWithUser($listname)) {<br /> $exceptionMessage = 'List %s is not associate with user %s ';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> $listname, <br /> $this->getUsername()<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> $_options['_method'] = 'DELETE';<br /> $path = '/1/%s/lists/%s.xml';<br /> $path = sprintf($path, <br /> $this->getUsername(), <br /> $this->_validListname($listname)<br /> );<br /> $response = $this->_post($path, $_options);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Adds a member to a list of the current user.<br /> *<br /> * @param integer $userId The numeric user id of the member to add.<br /> * @param string $listname The listname to add the member to.<br /> * @return Zend_Rest_Client_Result<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> public function addMember($userId, $listname)<br /> {<br /> $this->_init();<br /> <br /> if (!$this->_isListAssociatedWithUser($listname)) {<br /> $exceptionMessage = 'List %s is not associate with user %s ';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> $listname, <br /> $this->getUsername()<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> <br /> $_options['id'] = $this->_validInteger($userId); <br /> $path = '/1/%s/%s/members.xml';<br /> $path = sprintf($path, <br /> $this->getUsername(), <br /> $this->_validListname($listname)<br /> );<br /> <br /> if ($this->_isListMemberLimitReached($listname)) {<br /> $exceptionMessage = 'List can contain no more than %d members';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> self::LIST_MEMBER_LIMIT<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> <br /> $response = $this->_post($path, $_options);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Removes a member from a list of the current user.<br /> *<br /> * @param integer $userId The numeric user id of the member to remove.<br /> * @param string $listname The listname to remove the member from.<br /> * @return Zend_Rest_Client_Result<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> public function removeMember($userId, $listname)<br /> {<br /> $this->_init();<br /> <br /> if (!$this->_isListAssociatedWithUser($listname)) {<br /> $exceptionMessage = 'List %s is not associate with user %s ';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> $listname, <br /> $this->getUsername()<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> <br /> $_options['_method'] = 'DELETE';<br /> $_options['id'] = $this->_validInteger($userId); <br /> $path = '/1/%s/%s/members.xml';<br /> $path = sprintf($path, <br /> $this->getUsername(), <br /> $this->_validListname($listname)<br /> );<br /> $response = $this->_post($path, $_options);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Fetches the list members of the current user.<br /> *<br /> * @param string $listname The listname to fetch members from.<br /> * @return Zend_Rest_Client_Result<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> public function getMembers($listname) {<br /> $this->_init();<br /> $path = '/1/%s/%s/members.xml';<br /> $path = sprintf($path, <br /> $this->getUsername(), <br /> $this->_validListname($listname)<br /> );<br /> $response = $this->_get($path);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Fetches the list of the current user or any given user.<br /> *<br /> * @param string $username The username of the list owner.<br /> * @return Zend_Rest_Client_Result<br /> */<br /> public function getLists($username = null)<br /> {<br /> $this->_init();<br /> $path = '/1/%s/lists.xml';<br /> if (is_null($username)) {<br /> $path = sprintf($path, $this->getUsername());<br /> } else {<br /> $path = sprintf($path, $username);<br /> }<br /> $response = $this->_get($path);<br /> return new Zend_Rest_Client_Result($response->getBody());<br /> }<br /> /**<br /> * Checks if the list exists already to avoid number <br /> * indexed recreations.<br /> *<br /> * @param string $listname The list name.<br /> * @return boolean<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> private function _existsListAlready($listname)<br /> {<br /> $_listname = $this->_validListname($listname);<br /> $lists = $this->getLists();<br /> $_lists = $lists->lists;<br /> foreach ($_lists->list as $list) {<br /> if ($list->name == $_listname) {<br /> return true;<br /> }<br /> }<br /> return false;<br /> }<br /> /**<br /> * Checks if the list is associated with the current user.<br /> *<br /> * @param string $listname The list name.<br /> * @return boolean<br /> */<br /> private function _isListAssociatedWithUser($listname) <br /> {<br /> return $this->_existsListAlready($listname);<br /> }<br /> /**<br /> * Checks if the list member limit is reached.<br /> *<br /> * @param string $listname The list name.<br /> * @return boolean<br /> */<br /> private function _isListMemberLimitReached($listname)<br /> {<br /> $members = $this->getMembers($listname);<br /> return self::LIST_MEMBER_LIMIT < count($members->users->user);<br /> }<br /> /**<br /> * Returns the list creation mode or returns the private mode when invalid.<br /> * Valid values are private or public.<br /> *<br /> * @param string $creationMode The list creation mode.<br /> * @return string<br /> */<br /> private function _validMode($creationMode)<br /> {<br /> if (in_array($creationMode, array('private', 'public'))) {<br /> return $creationMode;<br /> }<br /> return 'private';<br /> }<br /> /**<br /> * Returns the list name or throws an Exception when invalid.<br /> *<br /> * @param string $listname The list name.<br /> * @return string<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> private function _validListname($listname)<br /> {<br /> $len = iconv_strlen(trim($listname), 'UTF-8');<br /> if (0 == $len) {<br /> $exceptionMessage = 'List name must contain at least one character';<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> } elseif (self::MAX_LIST_NAME_LENGTH < $len) {<br /> $exceptionMessage = 'List name must contain no more than %d characters';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> self::MAX_LIST_NAME_LENGTH<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> return trim($listname);<br /> }<br /> /**<br /> * Returns the list description or throws an Exception when invalid.<br /> *<br /> * @param string $description The list description.<br /> * @return string<br /> * @throws Zend_Service_Twitter_Exception<br /> */<br /> private function _validDescription($description)<br /> {<br /> $len = iconv_strlen(trim($description), 'UTF-8');<br /> if (0 == $len) {<br /> return '';<br /> } elseif (self::MAX_LIST_DESCRIPTION_LENGTH < $len) {<br /> $exceptionMessage = 'List description must contain no more than %d characters';<br /> $exceptionMessage = sprintf($exceptionMessage, <br /> self::MAX_LIST_DESCRIPTION_LENGTH<br /> );<br /> throw new Zend_Service_Twitter_Exception($exceptionMessage);<br /> }<br /> return trim(strip_tags($description));<br /> }<br />}</pre><h4 class="custom">Adding the 'auto list' feature</h4>For using the above implemented <em>add member</em> feature it's assumed that a participant has provided a valid and existing Twitter username, his approval of being added to the event list (i.e. zfweekend) and that he further has been registered effectively. To have the name of the Twitter list to act on and the account credentials available corresponding configuration entries are set as shown next.<br /><br /><div class="refactoringStatus" style="width: 205px;">application/configs/application.ini</div><pre class="codeSnippetRefactoring"><br />[production]<br />twitter.username = __USERNAME__<br />twitter.password = __PASSWORD__<br />twitter.auto.listname = zfweekend</pre>With the Twitter credentials and the list name available it's now possible to pull this feature into the register method of the register Action Controller, where it's applied as shown in the outro listing. As you will see, besides some bad practices due to demonstration purposes, the register Form makes use of a custom <em>TwitterScreenName</em> <a href="http://framework.zend.com/manual/en/zend.validate.html" target="_self">validator</a> and <a href="http://framework.zend.com/manual/en/zend.filter.html" target="_self">filter</a> which are also <a href="http://github.com/raphaelstolt/zf-hacks/tree/master/Recordshelf/" target="_self">available</a> via GitHub. Happy Twitter listing!<pre class="codeSnippet"><?php<br /><br />class RegisterController extends Zend_Controller_Action<br />{ <br /> /**<br /> * @badpractice Push this into a specific Form class.<br /> * @return Zend_Form<br /> */<br /> private function _getForm()<br /> {<br /> $form = new Zend_Form();<br /> $form->setAction('/register/register')<br /> ->setMethod('post');<br /> $twitterScreenName = $form->createElement('text', 'twitter_screen_name', <br /> array('label' => 'Twittername: ')<br /> );<br /> $twitterScreenName->addValidator(<em>new Recordshelf_Validate_TwitterScreenName()</em>)<br /> ->setRequired(true)<br /> ->setAllowEmpty(false) <br /> ->addFilter(<em>new Recordshelf_Filter_TwitterScreenName()</em>);<br /><br /> $autoListApproval = $form->createElement('checkbox', 'auto_list_approval', <br /> array('label' => 'I approved to be added to the event Twitter list: ')<br /> );<br /> <br /> $form->addElement($twitterScreenName)<br /> ->addElement($autoListApproval)<br /> ->addElement('submit', 'register', array('label' => ' Register '));<br /> <br /> return $form;<br /> }<br /> public function indexAction()<br /> {<br /> $this->view->form = $this->_getForm();<br /> } <br /> public function thanksAction()<br /> {<br /> }<br /> /**<br /> * @badpractice Handle possible Exception of<br /> * Recordshelf_Service_Twitter_List::addMember.<br /> * @return Recordshelf_Service_Twitter_List<br /> */<br /> public function registerAction()<br /> {<br /> $this->_helper->viewRenderer->setNoRender();<br /> $form = $this->_getForm();<br /> $request = $this->getRequest();<br /> <br /> if ($this->getRequest()->isPost()) {<br /> if ($form->isValid($request->getPost())) {<br /> <br /> $model = new Recordshelf_Model_Participant($form->getValues());<br /> $model->save();<br /><br /> if ($form->getElement('auto_list_approval')->isChecked()) {<br /> $twitterScreenName = $form->getValue('twitter_screen_name');<br /> $twitter = $this->_getTwitterListService();<br /> $response = $twitter->user->show($twitterScreenName);<br /> $userId = (string) $response->id;<br /> $response = $twitter->list->addMember($userId, <br /> $this->_getTwitterListName());<br /> <br /> $model->hasBeenAddedToTwitterList(true);<br /> $model->update();<br /><br /> return $this->_helper->redirector('thanks');<br /> }<br /> } else {<br /> return $this->_helper->redirector('index');<br /> }<br /> }<br /> }<br /> /**<br /> * @badpractice Push this into a dedicated Helper or something similar.<br /> * @return Recordshelf_Service_Twitter_List<br /> */<br /> private function _getTwitterListService()<br /> {<br /> $config = Zend_Registry::get('config');<br /> return new Recordshelf_Service_Twitter_List(<br /> $config->twitter->get('username'),<br /> $config->twitter->get('password')<br /> );<br /> }<br /> /**<br /> * @badpractice Push this into a dedicated Helper or something similar.<br /> * @return string<br /> */<br /> private function _getTwitterListName()<br /> {<br /> $config = Zend_Registry::get('config');<br /> return $config->twitter->auto->get('listname');<br /> }<br />}</pre><br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com5tag:blogger.com,1999:blog-8420118650236071171.post-75401604453954787342010-01-19T22:41:00.003+01:002010-02-23T10:43:36.524+01:00Closing and reopening GitHub issues via PHPUnit tests<img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 268px; height: 110px;" src="http://farm5.static.flickr.com/4010/4276887996_3190275b8b_o.gif" alt="PHPUnit GitHub TicketListener" title="PHPUnit GitHub TicketListener" border="0" /> Since <a href="http://www.phpunit.de" target="_self">PHPUnit</a> 3.4.0 a new extension point for interacting with issue tracking systems (TTS) based on the test results has been added to PHP's first choice <a href="http://en.wikipedia.org/wiki/XUnit" target="_self">xUnit</a> framework. The extension point has been introduced by an abstract PHPUnit_Extensions_TicketListener class, which allows developer to add tailor-made ticket listeners supporting their favoured TTS. Currently PHPUnit ships with a single ticket listener for <a href="http://trac.edgewall.org" target="_self">Trac</a> as it's still the used TTS for the framework itself. As I start to become more and more accustomed to use <a href="http://github.com" target="_self">GitHub</a> for some of my exploratory projects and hacks, the following blog post will contain a GitHub_TicketListener implementation and a showcase of it's usage.<h4 class="custom">Annotating tests with ticket meta data</h4>As you might know, it's considered to be a best practice to write a test for each new ticket representing a bug and drive the system under test (SUT) till the issue is resolved. This extension of test-driven development is also known as <a href="http://xunitpatterns.com/test-driven%20bug%20fixing.html" target="_self">test-driven bug fixing</a>. To create a relation between these tests and their associated tickets, PHPUnit provides a new @ticket annotation which will be analyzed before each test is run. The following code listing shows such an annotated test.<pre class="codeSnippet"><?php<br />require_once 'PHPUnit/Framework.php';<br /> <br />class ExampleTest extends PHPUnit_Framework_TestCase<br />{<br /> ....<br /><br /> /**<br /> * @ticket 2<br /> * @test<br /> */<br /> public function shouldGuarantyThatTheSutHandlesTheIssueCorrectly()<br /> {<br /> // test code<br /> }<br /> ....</pre><h4 class="custom">Peeking at the GitHub_TicketListener implementation</h4>The current version (3.4.6) of PHPUnit has a <a href="http://www.phpunit.de/ticket/953" target="_self">pending issue</a> regarding the abstract TicketListener class, so the first step is to apply an 'exploratory' <a href="http://gist.github.com/281297" target="_self">patch</a>, which might break the functionality of the shipped Trac ticket listener but will enable the use of the one for GitHub's TTS. <br /><br />The next step en route to a working GitHub_TicketListener is to extend the patched abstract PHPUnit_Extensions_TicketListener class. This abstract class contains two abstract methods named <em>getTicketInfo</em> and <em>updateTicket</em> which have to be implemented by the specific ticket listener class, and will be responsible for the interaction with the TTS.<br /><br />The implementation of the <em>getTicketInfo</em> method retrieves the ticket status for the annotated ticket, while the <em>updateTicket</em> method is responsible for changing the ticket status based on the test result and the former ticket state. Both implementations make use of the relevant <a href="http://develop.github.com/p/issues.html" target="_self">TTS part</a> of the <a href="http://develop.github.com" target="_self">GitHub API</a> by utilizing PHP's curl extension as shown in the next code listing which alternatively is available via this <a href="http://gist.github.com/281317" target="_self">gist</a>.<pre class="codeSnippet"><?php<br />require_once('PHPUnit/Extensions/TicketListener.php');<br />PHPUnit_Util_Filter::addFileToFilter(__FILE__, 'PHPUNIT');<br /><br />/**<br /> * A ticket listener that interacts with the GitHub issue API.<br /> */<br />class PHPUnit_Extensions_TicketListener_GitHub extends <br /> PHPUnit_Extensions_TicketListener<br />{<br /> const STATUS_CLOSE = 'closed';<br /> const STATUS_REOPEN = 'reopened';<br /> <br /> private $_username = null;<br /> private $_apiToken = null;<br /> private $_repository = null;<br /> private $_apiPath = null;<br /> private $_printTicketStateChanges = false;<br /> <br /> /**<br /> * @param string $username The username associated with the GitHub account.<br /> * @param string $apiToken The API token associated with the GitHub account.<br /> * @param string $repository The repository of the system under test (SUT) on GitHub.<br /> * @param string $printTicketChanges Boolean flag to print the ticket state <br /> * changes in the test result.<br /> * @throws RuntimeException<br /> */<br /> public function __construct($username, $apiToken, $repository, <br /> $printTicketStateChanges = false)<br /> {<br /> if ($this->_isCurlAvailable() === false) {<br /> throw new RuntimeException('The dependent curl extension is not available');<br /> }<br /> if ($this->_isJsonAvailable() === false) {<br /> throw new RuntimeException('The dependent json extension is not available');<br /> }<br /> $this->_username = $username;<br /> $this->_apiToken = $apiToken;<br /> $this->_repository = $repository;<br /> $this->_apiPath = 'http://github.com/api/v2/json/issues';<br /> $this->_printTicketStateChanges = $printTicketStateChanges;<br /> }<br /> <br /> /**<br /> * @param integer $ticketId <br /> * @return string<br /> * @throws PHPUnit_Framework_Exception<br /> */<br /> public function getTicketInfo($ticketId = null) <br /> {<br /> if (!ctype_digit($ticketId)) {<br /> return $ticketInfo = array('status' => 'invalid_ticket_id');<br /> } <br /> $ticketInfo = array();<br /> <br /> $apiEndpoint = "{$this->_apiPath}/show/{$this->_username}/"<br /> . "{$this->_repository}/{$ticketId}";<br /> <br /> $issueProperties = $this->_callGitHubIssueApiWithEndpoint($apiEndpoint, true);<br /><br /> if ($issueProperties['state'] === 'open') {<br /> return $ticketInfo = array('status' => 'new');<br /> } elseif ($issueProperties['state'] === 'closed') {<br /> return $ticketInfo = array('status' => 'closed');<br /> } elseif ($issueProperties['state'] === 'unknown_ticket') {<br /> return $ticketInfo = array('status' => $issueProperties['state']);<br /> }<br /> }<br /><br /> /**<br /> * @param string $ticketId The ticket number of the ticket under test (TUT).<br /> * @param string $statusToBe The status of the TUT after running the associated test.<br /> * @param string $message The additional message for the TUT.<br /> * @param string $resolution The resolution for the TUT.<br /> * @throws PHPUnit_Framework_Exception<br /> */<br /> protected function updateTicket($ticketId, $statusToBe, $message, $resolution)<br /> {<br /> $apiEndpoint = null;<br /> $acceptedResponseIssueStates = array('open', 'closed');<br /> <br /> if ($statusToBe === self::STATUS_CLOSE) {<br /> $apiEndpoint = "{$this->_apiPath}/close/{$this->_username}/"<br /> . "{$this->_repository}/{$ticketId}";<br /> } elseif ($statusToBe === self::STATUS_REOPEN) {<br /> $apiEndpoint = "{$this->_apiPath}/reopen/{$this->_username}/"<br /> . "{$this->_repository}/{$ticketId}";<br /> }<br /> if (!is_null($apiEndpoint)) {<br /> $issueProperties = $this->_callGitHubIssueApiWithEndpoint($apiEndpoint);<br /> if (!in_array($issueProperties['state'], $acceptedResponseIssueStates)) {<br /> throw new PHPUnit_Framework_Exception(<br /> 'Recieved an unaccepted issue state from the GitHub Api');<br /> }<br /> if ($this->_printTicketStateChanges) {<br /> printf("\nUpdating GitHub issue #%d, status: %s\n", $ticketId, <br /> $statusToBe);<br /> }<br /> }<br /> }<br /><br /> /**<br /> * @return boolean <br /> */<br /> private function _isCurlAvailable()<br /> {<br /> return extension_loaded('curl');<br /> }<br /><br /> /**<br /> * @return boolean <br /> */<br /> private function _isJsonAvailable()<br /> {<br /> return extension_loaded('json');<br /> }<br /><br /> /**<br /> * @param string $apiEndpoint API endpoint to call against the GitHub issue API.<br /> * @param boolean $isShowMethodCall Show method of the GitHub issue API is called? <br /> * @return array<br /> * @throws PHPUnit_Framework_Exception<br /> */<br /> private function _callGitHubIssueApiWithEndpoint($apiEndpoint, <br /> $isShowMethodCall = false) <br /> {<br /> $curlHandle = curl_init();<br /><br /> curl_setopt($curlHandle, CURLOPT_URL, $apiEndpoint);<br /> curl_setopt($curlHandle, CURLOPT_FOLLOWLOCATION, true);<br /> curl_setopt($curlHandle, CURLOPT_FAILONERROR, true);<br /> curl_setopt($curlHandle, CURLOPT_FRESH_CONNECT, true);<br /> curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);<br /> curl_setopt($curlHandle, CURLOPT_HTTPPROXYTUNNEL, true);<br /> curl_setopt($curlHandle, CURLOPT_USERAGENT, __CLASS__); <br /> curl_setopt($curlHandle, CURLOPT_POSTFIELDS,<br /> "login={$this->_username}&token={$this->_apiToken}");<br /><br /> $response = curl_exec($curlHandle);<br /> <br /> // Unknown tickets throw a 403 error<br /> if (!$response && $isGetTicketInfoCall) {<br /> return array('state' => 'unknown_ticket');<br /> }<br /><br /> if (!$response) {<br /> $curlErrorMessage = curl_error($curlHandle);<br /> $exceptionMessage = "A failure occured while talking to the "<br /> . "GitHub issue Api. {$curlErrorMessage}.";<br /> throw new PHPUnit_Framework_Exception($exceptionMessage);<br /> }<br /> $issue = (array) json_decode($response);<br /> $issueProperties = (array) $issue['issue'];<br /> curl_close($curlHandle);<br /> return $issueProperties;<br /> }<br />}</pre><h4 class="custom">Plugging the GitHub_TicketListener into the PHPUnit test environment</h4>To hook the GitHub ticket listener into the test runtime environment PHPUnit provides several approaches to do so. The chosen approach makes use of a <a href="http://www.phpunit.de/manual/current/en/appendixes.configuration.html" target="_self">XML configuration file</a> which allows an injection of the ticket listener in a declarative manner. As you will see in the configuration file snippet, the GitHub ticket listener is initialized with four parameters: The first one is the GitHub username, followed by the GitHub API token, the associated GitHub project, and a boolean flag for displaying the ticket status changes in the test result.<pre class="xmlSnippet"><phpunit><br /> <listeners><br /> <listener class="PHPUnit_Extensions_TicketListener_GitHub" <br /> file="/path/to/GitHubTicketListener.php"><br /> <arguments><br /> <string>raphaelstolt</string><br /> <string>API_TOKEN</string><br /> <string>PROJECT_NAME</string><br /> <boolean>true</boolean><br /> </arguments><br /> </listener><br /> </listeners><br /></phpunit></pre>To run the tests against a SUT and see the PHPUnit GitHub TTS interaction at work, all it takes is the forthcoming PHPUnit Cli call.<pre class="consoleOutput">phpunit --configuration github-ticketlistener.xml ExampleTest.php</pre>The outro screenshot shows the test result for an example SUT along with a GitHub TTS interaction due to a passing test which is associated with a open ticket in the TTS. <br /><br /><strong>A final note:</strong> As the interaction with an TTS adds some overhead to the test execution and thereby might cause <a href="http://xunitpatterns.com/Slow%20Tests.html" target="_self">Slow Tests</a>, ticket listener should only be considered in non time-critical test scenarios (e.g. nightly builds). <br/><br /><a href="http://www.flickr.com/photos/raphaelstolt/4288246677/" title="PHPUnit closing a GitHub issue"><img src="http://farm5.static.flickr.com/4034/4288246677_9d373ab585_o.gif" border="0" width="627" height="304" alt="PHPUnit closing a GitHub issue" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com5tag:blogger.com,1999:blog-8420118650236071171.post-76197832945188815152009-10-14T23:22:00.005+02:002009-10-15T00:02:23.950+02:00Zend Framework 1.8 Web Application Development book review<img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 190px; height: 228px;" src="http://farm3.static.flickr.com/2487/4011785234_2348120baa_o.gif" alt="Zend Framework 1.8 Web Application Development" title="Zend Framework 1.8 Web Application Development" border="0" />As the days are rapidly getting shorter, my reading appetite grows potentially and this evening I finished the 'Zend Framework 1.8 Web Application Development' book written by <a href="http://www.thepopeisdead.com" target="_self">Keith Pope</a>. While Keith worked on the book, I peeked several times at it's <a href="http://code.google.com/p/zendframeworkstorefront/" target="_self">tutorial application</a>, dubbed the Storefront, to get me going with the new Zend_Application component. Looking at it's code made me feel certain to get another great digest of the new features and components of version 1.8, and also a different practical perspective on web application development with the <a href="http://framework.zend.com/" target="_self">Zend Framework</a>, once the book has been published. Therefor I got in touch with the publisher <a href="http://www.packtpub.com" target="_self">Packt</a> and fortunately got a <a href="http://www.packtpub.com/zend-framework-1-8-web-application-development/book" target="_self">copy</a> of which I'd like to share a personal review in this blog post.<h4 class="custom">What's in it?</h4>The book opens with a quick run-through of the Model-View-Controller (MVC) architecture by creating a project structure via <a href="http://framework.zend.com/manual/en/zend.tool.framework.html" target="_self">Zend_Tool</a> and building a first very basic web application. While this introduction intentionally skips over a lot of details, the following chapter provides very detailed insights into the Zend Framework's MVC components by explaining the surrounded objects, the Design Patterns they are based upon and their interactions. <br /><br />After laying out that hefty block of theory the aforementioned tutorial application is introduced and built incrementally over several chapters; each one going into more detail for the specific application aspect. The highlight content of these chapters reach from introducing the Fat Model Skinny Controller concept, thoughts on Model design strategies which are reflected in a custom Storefront Model design, to developing application specific Front Controller Plugins, Action-Helpers, and View-Helpers. The application walk-through is completed by looking at general techniques to optimize the Storefront application and by building an automated <a href="http://www.phpunit.de" target="self">PHPUnit</a> Test Suite of functional tests utilizing <a href="http://framework.zend.com/manual/en/zend.test.html" target="_self">Zend_Test</a> to keep the Zend Framework based application self-reliant and refactorable.<h4 class="custom">Conclusion</h4>The book by Keith Pope provides any interested PHP developer, who's not already sold on a specific framework, a thorough introduction to the vivid Zend Framework and it's use in a MVC based web application development context. The content of the book is delivered in a fluent, very enthusiastic and 'knowledge-pillowed' writing tone. By implementing or working through the Storefront application seasoned web developers using older versions of the Framework will get a good blue sheet on new components like <a href="http://framework.zend.com/manual/en/zend.application.html" target="_self">Zend_Application</a> and it's implication in the bootstrapping process; while new developers tending towards picking up the Zend Framework will get a current and well compiled guide, which might first start off with a steep learning-curve but will turn into profund knowledge once hanging in there.<br /><br />The only thing that seemed a bit odd to me, was the utilization of <a href="http://ant.apache.org" target="_self">Ant</a> instead of <a href="http://phing.info" target="_self">Phing</a> as the build tool for the Storefront application to set the application environment, to remove all require_once statements from the framework library and to run the PHPUnit Test Suite; but this might also be inflicted by my Phing <a href="http://raphaelstolt.blogspot.com/search/label/Phing" target="_self">nuttiness</a>.<br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com6tag:blogger.com,1999:blog-8420118650236071171.post-65284400240794345032009-09-19T21:00:00.018+02:002011-11-09T13:05:29.004+01:00Logging to MongoDb and accessing log collections with Zend_ToolInfluenced by a recent <a href="http://taukon.de/2009/zend_log-mit-mongodb/" target="_self" title="Zend_Log mit mongoDB">blog post</a> of a colleague of mine and by being kind of broke on a Saturday night; I tinkered with the just recently discovered <a href="http://www.mongodb.org/display/DOCS/Home" target="_self">MongoDb</a> and hooked it into the <a href="http://framework.zend.com/manual/en/zend.log.html" target="_self">Zend_Log</a> environment by creating a dedicated Zend_Log_Writer. The following post will therefore present a peek at a <em>prototypesque</em> implementation of this writer and show how the afterwards accumulated log entries can be accessed and filtered with a <a href="http://raphaelstolt.blogspot.com/2009/07/scaffolding-implementing-and-using.html" target="_self" title="Scaffolding, implementing and using project specific Zend_Tool_Project_Providers">custom Zend_Tool project provider</a>.<h4 class="custom">Logging to a MongoDb database</h4>The following steps assume that an instance of a MongoDb server is running and that the required PHP MongoDb module is also <a href="http://www.mongodb.org/display/DOCS/Installing+the+PHP+Driver" target="_self">installed</a> and loaded. To by-pass log entries to a MongoDb database there is a need to craft a proper Zend_Log_Writer. This can be achieved by extending the Zend_Log_Writer_Abstract class, injecting a <a href="http://php.net/manual/en/class.mongo.php" target="_self">Mongo connection</a> instance and implementing the actual write functionality as shown in the next listing.<pre class="codeSnippet"><?php
require_once 'Zend/Log/Writer/Abstract.php';
class Recordshelf_Log_Writer_MongoDb extends Zend_Log_Writer_Abstract
{
private $_db;
private $_connection;
/**
* @param Mongo $connection The MongoDb database connection
* @param string $db The MongoDb database name
* @param string $collection The collection name string the log entries
*/
public function __construct(Mongo $connection, $db, $collection)
{
$this->_connection = $connection;
$this->_db = $this->_connection->selectDB($db)->createCollection(
$collection
);
}
public function setFormatter($formatter)
{
require_once 'Zend/Log/Exception.php';
throw new Zend_Log_Exception(get_class() . ' does not support formatting');
}
public function shutdown()
{
$this->_db = null;
$this->_connection->close();
}
protected function _write($event)
{
$this->_db->insert($event);
}
/**
* Create a new instance of Recordshelf_Log_Writer_MongoDb
*
* @param array|Zen_Config $config
* @return Recordshelf_Log_Writer_MongoDb
* @throws Zend_Log_Exception
* @since Factory Interface available since release 1.10.0
*/
static public function factory($config)
{
$exceptionMessage = 'Recordshelf_Log_Writer_MongoDb does not currently '
. 'implement a factory';
throw new Zend_Exception($exceptionMessage);
}
}</pre>With the MongoDb writer available and added to the library directory of the application it's now possible to utilize this new storage backend as usual with the known Zend_Log component. The Mongo connection injected into the writer is configured via <a href="http://framework.zend.com/manual/en/zend.config.html" target="_self">Zend_Config</a> and initialized via the <a href="http://framework.zend.com/manual/en/zend.application.html" target="_self">Zend_Application</a> bootstrapping facility as shown in the listings below.<br />
<br />
<div class="refactoringStatus" style="width: 205px;">application/configs/application.ini</div><pre class="codeSnippetRefactoring">[production]
app.name = recordshelf
....
log.mongodb.db = zf_mongo
log.mongodb.collection = recordshelf_log
log.mongodb.server = localhost
log.priority = Zend_Log::CRIT
....</pre><div class="refactoringStatus" style="width: 154px;">application/Bootstrap.php</div><pre class="codeSnippetRefactoring"><?php
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
protected $_logger;
protected function _initConfig()
{
Zend_Registry::set('config', new Zend_Config($this->getOptions()));
}
protected function _initLogger()
{
$this->bootstrap(array('frontController', 'config'));
$config = Zend_Registry::get('config');
$applicationName = $config->app->get('name', 'recordshelf');
$mongoDbServer = $config->log->mongodb->get('server', '127.0.0.1');
$mongoDbName = $config->log->mongodb->get('db', "{$applicationName}_logs");
$mongoDbCollection = $config->log->mongodb->get('collection', 'entries');
$logger = new Zend_Log();
$writer = new Recordshelf_Log_Writer_MongoDb(new Mongo($mongoDbServer),
$mongoDbName, $mongoDbCollection);
if ('production' === $this->getEnvironment()) {
$priority = constant($config->log->get('priority', Zend_Log::CRIT));
$filter = new Zend_Log_Filter_Priority($priority);
$logger->addFilter($filter);
}
$logger->addWriter($writer);
$this->_logger = $logger;
Zend_Registry::set('log', $logger);
}
}</pre><div class="refactoringStatus" style="width: 205px;">controllers/ExampleController.php</div><pre class="codeSnippetRefactoring"><?php
class ExampleController extends Zend_Controller_Action
{
private $_logger = null;
public function init()
{
$this->_logger = Zend_Registry::get('log');
}
public function fooAction()
{
$this->_logger->log('A debug log message from within action ' .
$this->getRequest()->getActionName(), Zend_Log::DEBUG);
}
public function barAction()
{
$this->_logger->log('A debug log message from within ' .
__METHOD__, Zend_Log::DEBUG);
}
}</pre><h4 class="custom">Accessing the log database with a Zend_Tool project provider</h4>After handling the application-wide logging with the MongoDb writer sooner or later the issue to access the gathered log entries will rise. For this mundane and recurring use case the ProjectProvider provider of the Zend_Tool framework is an acceptable candidate to hook a custom action into the Zend_Tool environment of a given project. Therefor a new Zend_Tool_Project Project provider is first scaffolded via the forthcoming command.<pre class="consoleOutput">sudo zf create project-provider mongodb-logs filter</pre>Second the generated provider skeleton its filter action is enliven with the logic to query the MongoDb database and the stored log collection. The action to come accepts three arguments to filter the stored log entry results by a specific date in the format of 'YYYY-MM-DD' and a given Zend_Log priority (currently limited to the constants defined in Zend_Log) in a specific application environment. The next listing shows the implementation of the import action of the MongodbLogsProvider project provider; which is clearly, as it's length indicates, in need for a clean-up task.<br />
<br />
<div class="refactoringStatus" style="width: 225px;">providers/Mongodb-logsProvider.php</div><pre class="codeSnippetRefactoring"><?php
require_once 'Zend/Tool/Project/Provider/Abstract.php';
require_once 'Zend/Tool/Project/Provider/Exception.php';
require_once 'Zend/Date.php';
require_once 'Zend/Validate/Date.php';
require_once 'Zend/Log.php';
require_once 'Zend/Config/Ini.php';
class MongodbLogsProvider extends Zend_Tool_Project_Provider_Abstract
{
public function filter($date = null, $logPriority = null,
$env = 'development')
{
$ref = new Zend_Reflection_Class('Zend_Log');
$logPriorities = $ref->getConstants();
if (in_array(strtoupper($date), array_keys($logPriorities)) ||
in_array(strtoupper($date), array_values($logPriorities))) {
$logPriority = $date;
$date = null;
}
if (!is_null($date)) {
$validator = new Zend_Validate_Date();
if (!$validator->isValid($date)) {
$exceptionMessage = "Given date '{$date}' is not a valid date.";
throw new Zend_Tool_Project_Provider_Exception($exceptionMessage);
}
$dateArray = array();
list($dateArray['year'], $dateArray['month'], $dateArray['day']) =
explode('-', $date);
$date = new Zend_Date($dateArray);
} else {
$date = new Zend_Date();
}
$date = $date->toString('Y-MM-dd');
if (!is_null($logPriority)) {
if (!is_numeric($logPriority)) {
$logPriority = strtoupper($logPriority);
if (!in_array($logPriority, array_keys($logPriorities))) {
$exceptionMessage = "Given priority '{$logPriority}' is not defined.";
throw new Zend_Tool_Project_Provider_Exception($exceptionMessage);
} else {
$logPriority = $logPriorities[$logPriority];
}
}
if (!in_array($logPriority, array_values($logPriorities))) {
$exceptionMessage = "Given priority '{$logPriority}' is not defined.";
throw new Zend_Tool_Project_Provider_Exception();
}
$priorities = array_flip($logPriorities);
$priorityName = $priorities[$logPriority];
}
if ($env !== 'development' && $env !== 'production') {
$exceptionMessage = "Unsupported environment '{$env}' provided.";
throw new Zend_Tool_Project_Provider_Exception();
}
$config = new Zend_Config_Ini('./application/configs/application.ini',
$env);
$applicationName = $config->app->get('name', 'recordshelf');
$mongoDbServer = $config->log->mongodb->get('server', '127.0.0.1');
$mongoDbName = $config->log->mongodb->get('db', "{$applicationName}_logs");
$mongoDbCollection = $config->log->mongodb->get('collection', 'entries');
try {
$connection = new Mongo($mongoDbServer);
$db = $connection->selectDB($mongoDbName)->createCollection(
$mongoDbCollection);
} catch (MongoConnectionException $e) {
throw new Zend_Tool_Project_Provider_Exception($e->getMessage());
}
$dateRegex = new MongoRegex("/$date.*/i");
if (is_null($logPriority)) {
$query = array('timestamp' => $dateRegex);
$appendContentForResults = "Found #amountOfEntries# log entrie(s) "
. "on {$date}";
$appendContentForNoResults = "Found no log entries on {$date}";
} else {
$query = array('priority' => (int) $logPriority,
'timestamp' => $dateRegex
);
$appendContentForResults = "Found #amountOfEntries# log entrie(s) "
. "for priority {$priorityName} on {$date}";
$appendContentForNoResults = "Found no log entries for priority "
. "{$priorityName} on {$date}";
}
$cursor = $db->find($query);
$amountOfEntries = $cursor->count();
if ($amountOfEntries > 0) {
$content = str_replace('#amountOfEntries#', $amountOfEntries,
$appendContentForResults);
$this->_registry->getResponse()->appendContent($content);
foreach ($cursor as $id => $value) {
$content = "{$id}: {$value['timestamp']} > ";
if (is_null($logPriority)) {
$content.= "[{$value['priorityName']}] ";
}
$content.= "{$value['message']}";
$this->_registry->getResponse()->appendContent($content);
}
} else {
$content = $appendContentForNoResults;
$this->_registry->getResponse()->appendContent($content);
}
$connection->close();
}
}</pre>The coming outro screenshots show two use cases for the filter action of the MongodbLogsProvider issued against the zf command line client. The first screenshot shows the use case where all log entries for the current day are queried, while the second one shows the use case where all log entries for a specific date and log priority are queried and fed back to the user.<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/3934270869/" title="All log entries of the current day by Raphael Stolt, on Flickr"><img src="http://farm3.static.flickr.com/2548/3934270869_fa329bea00_o.gif" width="800" height="380" border="0" alt="All log entries of the current day" /></a><br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/3935054512/" title="All CRIT log entries for a specific date by Raphael Stolt, on Flickr"><img src="http://farm3.static.flickr.com/2658/3935054512_e929053892_o.gif" width="613" height="170" border="0" alt="All CRIT log entries for a specific date" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com3tag:blogger.com,1999:blog-8420118650236071171.post-49875939600437474312009-08-22T15:08:00.004+02:002009-08-24T11:20:55.106+02:00Kicking off custom Phing task development with TextMateAs a reader of this blog you migth have noticed that from time to time I like to utilize <a href="http://phing.info" target="_self">Phing</a>'s ability to write custom tasks. Though that's not an everyday routine for me and therefor I might, depending on my form of the day, end up with some real smelly code where for example the task's properties validation is handled in the task's main worker method. This is actually a bad habit/practice I'm aware of and to improve my future endeavours in custom Phing task development, I bended <a href="http://macromates.com/" target="_self">TextMate</a>'s snippet feature to my needs.<br /><br /><a href="http://manual.macromates.com/en/snippets#snippets" target="_self">Snippets</a> in TextMate are a very powerful feature that can be used to insert code that you do not want to type again and again, or like in my case might have forgotten over a certain time.<br /><br />The next code listing shows the snippet providing a basic custom Phing task class skeleton which can be utilized over and over at the beginning of the implementation activities.<pre class="codeSnippet"><?php<br />require_once 'phing/Task.php';<br /><br />class ${1:CustomName}Task extends Task<br />{<br /> private \$_${2:property} = null;<br /> <br /> /**<br /> * @param string \$${2:property} ${3:description}<br /> */<br /> public function set${2/./\u$0/}(\$${2:property})<br /> {<br /> \$this->_${2:property} = trim(\$${2:property});<br /> }<br /> /**<br /> * Initializes the task environment if necessary<br /> */<br /> public function init()<br /> {<br /> }<br /> /**<br /> * Does the task main work or delegates it<br /> * @throws BuildException<br /> */<br /> public function main()<br /> {<br /> \$this->_validateProperties();<br /> }<br /> /**<br /> * Validates the task properties<br /> * @throws BuildException<br /> */<br /> private function _validateProperties()<br /> {<br /> if (is_null(\$this->_${2:property})) {<br /> throw new BuildException('${4:message}.');<br /> }$0<br /> }<br />}</pre>To apply the snippet, after installing it, on a PHP source file it can either be selected from the <a href="http://manual.macromates.com/en/bundles#bundles" target="_self">Bundles</a> menue or more comfortable via the assigned tab trigger i.e. <em>ctask</em>. After triggering the snippet it's possible to properly name the task under development and dynamically set it's first property, which is also treated as a mandatory property in the extracted <em>_validateProperties</em> method.<br /><br />The outro image shows the above stated snippet in the TextMate Bundle Editor and it's configuration.<br /><br /><a href="http://www.flickr.com/photos/raphaelstolt/3844647803/" title="Phing snippet in the TextMate Bundle Editor by Raphael Stolt, on Flickr"><img src="http://farm4.static.flickr.com/3550/3844647803_3175559fed_o.gif" width="616" height="431" border="0" alt="Phing snippet in the TextMate Bundle Editor" /></a><br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com1tag:blogger.com,1999:blog-8420118650236071171.post-29740534217309733112009-07-04T16:41:00.011+02:002009-07-06T08:05:42.909+02:00Scaffolding, implementing and using project specific Zend_Tool_Project_ProvidersWorking on a project involving several legacy data migration tasks, I got curious what the <a href="http://framework.zend.com/manual/en/zend.tool.project.html" target="_self">Zend_Tool_Project</a> component of the <a href="http://framework.zend.com" target="_self">Zend Framework</a> 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.<h4 class="custom">Scaffolding project specific providers</h4>All following steps assume there is a project available i.e. <em>recordshelf</em> 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 <em>create</em> action of the <em>ProjectProvider</em> provider by passing in the name of the provider i.e. <em>csv</em> and it's intended actions. As the next console snippet shows it's <br />possible to specify several actions as a comma separated list.<pre class="consoleOutput">sudo zf create project-provider csv importSpecials,importSummersale</pre>After running the command the project's profile <em>.zfproject.xml</em> has been modified and a new <em>providers</em> 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 <em>importSpecials</em> and <em>importSummersale</em>. 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 <em>.zfproject.xml</em> is incorrect, but can be fixed by renaming the class from CsvProvider to Csv.<pre class="codeSnippet"><?php<br /><br />require_once 'Zend/Tool/Project/Provider/Abstract.php';<br />require_once 'Zend/Tool/Project/Provider/Exception.php';<br /><br />class Csv<strike style="font-weight: bolder;">Provider</strike> extends Zend_Tool_Project_Provider_Abstract<br />{<br /><br /> public function importSpecials()<br /> {<br /> /** @todo Implementation */<br /> }<br /><br /> public function importSummersale()<br /> {<br /> /** @todo Implementation */<br /> }<br /><br /><br />}</pre><h4 class="custom">Implementing the action logic</h4>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 <em>importSpecials</em> 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.<pre class="codeSnippet"><?php<br /><br />require_once 'Zend/Tool/Project/Provider/Abstract.php';<br />require_once 'Zend/Tool/Project/Provider/Exception.php';<br /><br />class Csv extends Zend_Tool_Project_Provider_Abstract<br />{<br /> private function _isProjectProviderSupportedInProject(Zend_Tool_Project_Profile $profile, <br /> $projectProviderName)<br /> {<br /> $projectProviderResource = $this->_getProjectProfileResource($profile, <br /> $projectProviderName);<br /> return $projectProviderResource instanceof Zend_Tool_Project_Profile_Resource;<br /> }<br /> <br /> private function _isActionSupportedByProjectProvider(Zend_Tool_Project_Profile $profile, <br /> $projectProviderName, $actionName)<br /> {<br /> $projectProviderResource = $this->_getProjectProfileResource($profile, <br /> $projectProviderName);<br /> $projectProviderAttributes = $projectProviderResource->getContext()<br /> ->getPersistentAttributes();<br /> return in_array($actionName, explode(',', $projectProviderAttributes['actionNames']));<br /> }<br /> <br /> private function _getProjectProfileResource(Zend_Tool_Project_Profile $profile, <br /> $projectProviderName)<br /> {<br /> $profileSearchParams[] = 'ProjectProvidersDirectory';<br /> $profileSearchParams['ProjectProviderFile'] = <br /> array('projectProviderName' => strtolower($projectProviderName));<br /> return $profile->search($profileSearchParams); <br /> }<br /><br /> <em style="font-weight: bolder;">public function importSpecials($csvFile, $env = 'development')<br /> {<br /> $relatedTablename = 'specials';<br /> <br /> if (!$this->_isProjectProviderSupportedInProject($profile, __CLASS__)) {<br /> throw new Exception("ProjectProvider Csv is not supported in this project.");<br /> }<br /> if (!$this->_isActionSupportedByProjectProvider($profile, __CLASS__, __FUNCTION__)) {<br /> $exceptionMessage = "Action 'importSpecials' is not supported by "<br /> . "the Csv ProjectProvider in this project.";<br /> throw new Exception($exceptionMessage);<br /> }<br /><br /> if (!file_exists($csvFile)) {<br /> throw new Exception("Given csv-file '{$csvFile}' doesn't exist.");<br /> }<br /> <br /> $importEnvironment = trim($env);<br /> if ($importEnvironment !== 'development' && $importEnvironment !== 'production') {<br /> throw new Exception("Unsupported environment '{$importEnvironment}' provided.");<br /> }<br /> <br /> $csvHandle = fopen($csvFile, "r");<br /> <br /> if (!$csvHandle) {<br /> throw new Exception("Unable to open given csv-file '{$csvFile}'.");<br /> }<br /> <br /> $config = new Zend_Config_Ini('./application/configs/application.ini', <br /> $importEnvironment);<br /> $db = Zend_Db::factory($config->database);<br /><br /> $db->query("TRUNCATE TABLE {$relatedTablename}");<br /> echo "Truncated the project '{$relatedTablename}' database table." . PHP_EOL;<br /> <br /> $rowCount = $insertCount = 0;<br /> <br /> while (($csvLine = fgetcsv($csvHandle)) !== false) {<br /> if ($rowCount > 0) {<br /> $insertRow = array(<br /> 'product_name' => $csvLine[0],<br /> 'product_image_path' => $csvLine[1],<br /> 'price' => $csvLine[2],<br /> 'special_until' => $csvLine[3]<br /> );<br /> $db->insert($relatedTablename, $insertRow);<br /> ++$insertCount;<br /> }<br /> ++$rowCount;<br /> }<br /> fclose($csvHandle);<br /> $importMessage = "Imported {$insertCount} rows into the project "<br /> . "'{$relatedTablename}' database table.";<br /> echo $importMessage;<br /> }</em><br /> ...<br />}</pre><h4 class="custom">Making providers and actions pretendable</h4>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 <em>Zend_Tool_Framework_Provider_Pretendable</em>. 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 <em>-p</em> option to the issued zf command line client command. The next code snippet shows how the above stated <em>Csv</em> provider and its <em>importSpecials</em> action is made pretendable.<pre class="codeSnippet"><?php<br /><br />require_once 'Zend/Tool/Project/Provider/Abstract.php';<br />require_once 'Zend/Tool/Project/Provider/Exception.php';<br /><br />class Csv extends Zend_Tool_Project_Provider_Abstract<em style="font-weight: bolder;"> implements <br /> Zend_Tool_Framework_Provider_Pretendable</em><br />{<br /><br /> public function importSpecials($csvFile, $env = 'development')<br /> {<br /> ...<br /> <br /> <em style="font-weight: bolder;">if ($this->_registry->getRequest()->isPretend()) {<br /> $pretendMessage = "I would import the specials data provided in {$csvFile} "<br /> . "into the project '{$relatedTablename}' database table.";<br /> echo $pretendMessage;<br /> } else {<br /> ... <br /> }</em><br /> }<br /> ...<br />}</pre><h4 class="custom">Using project specific providers</h4>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 <em>include_path</em>. 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 <em>zf --help</em> 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 <em>importSpecials</em> action in one of above code snippets.<br /><br /><a href="http://www.flickr.com/photos/raphaelstolt/3687344640/" title="Provider overview by Raphael Stolt, on Flickr"><img src="http://farm3.static.flickr.com/2630/3687344640_510d00350a_o.gif" width="606" height="282" border="0" alt="Provider overview" /></a><br /><br />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.<br /><br /><a href="http://www.flickr.com/photos/raphaelstolt/3686545463/" title="Calling the import-specials action by Raphael Stolt, on Flickr"><img src="http://farm3.static.flickr.com/2584/3686545463_c93b1b18de_o.gif" width="606" height="282" border="0" alt="Calling the import-specials action" /></a><br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-51100829201057134272009-05-10T16:15:00.017+02:002011-02-18T17:45:04.272+01:00Testing Phing buildfiles with PHPUnitWhile <a href="http://raphaelstolt.blogspot.com/2008/07/six-valuable-phing-build-file.html" target="_self">transforming</a> some of the <a href="http://ant.apache.org/" target="_self">Ant</a> buildfile refactorings described in <a href="http://www.build-doctor.com/" target="_self">Julian Simpson</a>'s seminal essay into a <a href="http://phing.info/trac/" target="_self">Phing</a> context, it felt plainly wrong that I didn't have any tests for the buildfile to back me up on obtaining the pristine behaviour throughout the process. While Ant users can rely on an Apache project called <a href="http://ant.apache.org/antlibs/antunit/">AntUnit</a> there are currently no tailor-made tools available for testing or verifying Phing buildfiles. Therefor I took a weekend off, locked myself in the stuffy lab, and explored the abilities to test Phing buildfiles respectively their included properties, targets and tasks with the <a href="http://phpunit.de/" target="_self">PHPUnit</a> testing framework. In case you'd like to take a peek at the emerged <b>lab jottings</b>, keep on scanning.<br />
<h4 class="custom">Introducing the buildfile under test</h4>The buildfile that will be used as an example is kept simple, and contains several targets ranging from common ones like initializing the build environment by creating the necessary directories to more specific ones like pulling an external artifact from <a href="http://github.com/" target="_self">GitHub</a>. To get an overview of the buildfile under test have a look at the following listing.<br />
<pre class="xmlSnippet"><?xml version="1.0" encoding="UTF-8"?>
<project name="test-example" default="build" basedir=".">
<property name="project.basedir" value="." override="true" />
<property name="github.repos.dir" value="${project.basedir}/build/github-repos" override="true" />
<target name="clean" depends="clean-github-repos" description="Removes runtime build artifacts">
<delete dir="${project.basedir}/build" includeemptydirs="true" verbose="false" failonerror="true" />
<delete dir="${project.basedir}/build/reports" includeemptydirs="true" verbose="false" failonerror="true" />
</target>
<target name="clean-github-repos" description="Removes runtime build artifacts">
<delete dir="${github.repos.dir}" includeemptydirs="true" failonerror="true" />
</target>
<target name="-log-build" description="A private target which should only be invoked internally">
<!-- omitted -->
</target>
<target name="build" depends="clean" description="Builds the distributable product">
<!-- omitted -->
</target>
<target name="database-setup" description="Sets up the database structure">
<!-- omitted -->
</target>
<target name="init" description="Initalizes the build by creating directories etc">
<mkdir dir="${project.basedir}/build/logs/performance/" />
<mkdir dir="${project.basedir}/build/doc" />
<mkdir dir="${project.basedir}/build/reports/phploc" />
</target>
<target name="init-ad-hoc-tasks"
description="Initalizes the ad hoc tasks for reusability in multiple targets">
<adhoc-task name="github-clone"><![CDATA[
class Github_Clone extends Task {
private $repository = null;
private $destDirectory = null;
function setRepos($repository) {
$this->repository = $repository;
}
function setDest($destDirectory) {
$this->destDirectory = $destDirectory;
}
function main() {
// Get project name from repos Uri
$projectName = str_replace('.git', '',
substr(strrchr($this->repository, '/'), 1));
$gitCommand = 'git clone ' . $this->repository . ' ' .
$this->destDirectory . '/' . $projectName;
exec(escapeshellcmd($gitCommand), $output, $return);
if ($return !== 0) {
throw new BuildException('Git clone failed');
}
$logMessage = 'Cloned Git repository ' . $this->repository .
' into ' . $this->destDirectory . '/' . $projectName;
$this->log($logMessage);
}
}
]]></adhoc-task>
<echo message="Intialized github-clone ad hoc task." />
</target>
<target name="github" depends="init-ad-hoc-tasks, clean-github-repos"
description="Clones given repositories from GitHub">
<github-clone repos="git://github.com/raphaelstolt/phploc-phing.git" dest="${github.repos.dir}" />
</target>
</project></pre><h4 class="custom">Testing the buildfile</h4>All tests for the buildfile under test will be bundled, like 'normal' tests, in a class i.e. BuildfileTest extending the PHPUnit_Framework_TestCase class. When testing buildfiles it's possible to build some tests around the actual buildfile XML <b>structure</b>, by utilizing the <i>xpath</i> method of PHP's SimpleXMLElement class and asserting against the <a href="http://www.w3.org/TR/xpath" target="_self">XPath</a> query results, or around the dispatching of specific targets and asserting against the expected build <b>artifacts</b>. Furthermore these two identified groups, structure and artifact, can be used to <a href="http://mikenaberezny.com/2007/09/04/better-phpunit-group-annotations/" target="_self">organize</a> the accumulating tests via PHPUnit's @group annotation.<br />
<br />
To be able to dispatch specific build targets and feed them with properties if necessary I additionally developed a <i>very</i> basic build runner shown in the next code listing.<br />
<pre class="codeSnippet"><?php
class Phing_Buildfile_Runner {
private $_buildfilePath = null;
public function __construct($buildfilePath) {
if (!file_exists($buildfilePath)) {
throw new Exception("Buildfile '{$buildfilePath}' doesn't exist");
}
$this->buildfilePath = realpath($buildfilePath);
}
public function runTarget($targets = array(), $properties = array()) {
$runTargetCommand = "phing " . "-f {$this->buildfilePath} ";
if (count($targets) > 0) {
foreach ($targets as $target) {
$runTargetCommand.= $target . " ";
}
}
if (count($properties) > 0) {
foreach ($properties as $property => $value) {
$runTargetCommand.= "-D{$property}={$value} ";
}
}
exec(escapeshellcmd($runTargetCommand), $output, $return);
return array('output' => $output, 'return' => $return);
}
}</pre>Out of the box PHPUnit's <a href="http://www.phpunit.de/manual/current/en/api.html#api.assert" target="_self">assertion pool</a> provides all the utilities to test buildfiles; although it would be cleaner to <a href="http://raphaelstolt.blogspot.com/2008/07/creating-custom-phpunit-assertions.html" target="_self">create domain specfic assertions</a> for this testing domain this technique will be ignored for the sake of brevity.<br />
<br />
After an initial 1000ft view on how to test buildfiles let's jump into the actual testing of a <b>structural</b> aspect of the buildfile under test. The test to come shows how to verify that a clean target is defined for playing along in the build orchestra by querying a XPath expression against the buildfile XML and asserting that a result is available.<br />
<pre class="codeSnippet">/**
* @test
* @group structure
*/
public function buildfileShouldContainACleanTarget() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$cleanElement = $xml->xpath("//target[@name='clean']");
$this->assertTrue(count($cleanElement) > 0, "Buildfile doesn't contain a clean target");
}</pre>The next <b>artifactual</b> test raises the bar an inch, by verifying that the defined init target of the build does initialize the build environment correctly, or to pick up the orchestra metaphor again that the specific instrument plays along and holds the directed tone. Therefor the build runner executes the target and afterwards asserts a list of expected artifacts against the current state of the build process.<br />
<pre class="codeSnippet">/**
* @test
* @group artifact
*/
public function initTargetShouldCreateInitialBuildArtifacts() {
$this->_isTearDownNecessary = true;
$this->_buildfileRunner->runTarget(array('init'));
$expectedInitArtifacts = array(
"{$this->_buildfileBasedir}/build",
"{$this->_buildfileBasedir}/build/logs/performance/",
"{$this->_buildfileBasedir}/build/doc",
"{$this->_buildfileBasedir}/build/reports"
);
foreach ($expectedInitArtifacts as $artifact) {
$this->assertFileExists($artifact, "Expected file '{$artifact}' doesn't exist");
}
}</pre>The next code listing shows the whole picture of the BuildfileTest class containing additional test methods verifying different aspects of the buildfile under test and also the innards of the setup and teardown method.<br />
<br />
The main assignment of the setup method is to load the XML of the buildfile under test and to intialize the build runner so an instance is available for an use in artifactual tests. The teardown method its sole responsibility is to reset the build state by running the clean target of the buildfile.<br />
<pre class="codeSnippet"><?php
require_once 'PHPUnit/Framework.php';
require_once 'Phing/Buildfile/Runner.php';
class ExampleBuildfileTest extends PHPUnit_Framework_TestCase {
protected $_buildfileXml = null;
protected $_buildfileName = null;
protected $_buildfileBasedir = null;
protected $_buildfileRunner = null;
protected $_isTearDownNecessary = false;
protected function setUp() {
$this->_buildfileName = realpath('../../build.xml');
$this->_buildfileBasedir = dirname($this->_buildfileName);
$this->_buildfileXml = file_get_contents($this->_buildfileName);
$this->_buildfileRunner = new Phing_Buildfile_Runner(
$this->_buildfileName);
}
protected function tearDown() {
if ($this->_isTearDownNecessary) {
$this->_buildfileRunner->runTarget(array('clean'));
}
}
/**
* @test
* @group structure
*/
public function targetBuildShouldBeTheDefaultTarget() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$xpath = "//@default";
$defaultElement = $xml->xpath($xpath);
$this->assertSame('build', trim($defaultElement[0]->default),
"Buildfile doesn't have a default target named 'build'"
);
}
/**
* @test
* @group structure
*/
public function propertyGithubReposDirShouldBeSet() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$xpath = "//property[@name='github.repos.dir']/@value";
$valueElement = $xml->xpath($xpath);
$this->assertTrue($valueElement[0] instanceof SimpleXMLElement,
"Buildfile doesn't contain a 'github.repos.dir' property"
);
$this->assertGreaterThan(1, strlen($valueElement[0]->value));
}
/**
* @test
* @group structure
*/
public function buildfileShouldContainACleanTarget() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$cleanElement = $xml->xpath("//target[@name='clean']");
$this->assertTrue(count($cleanElement) > 0,
"Buildfile doesn't contain a clean target"
);
}
/**
* @test
* @group structure
*/
public function targetLogBuildShouldBeAPrivateOne() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$nameElement = $xml->xpath("//target[@name='-log-build']");
$this->assertTrue(count($nameElement) > 0,
'Log build target is not a private target'
);
}
/**
* @test
* @group structure
*/
public function targetBuildShouldDependOnCleanTarget() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$xpath = "//target[@name='build']/@depends";
$dependElement = $xml->xpath($xpath);
$this->assertTrue(count($dependElement) > 0,
'Target build contains no depends attribute'
);
$dependantTasks = array_filter(explode(' ',
trim($dependElement[0]->depends))
);
$this->assertContains('clean', $dependantTasks, "Target build doesn't
depend on the clean target"
);
}
/**
* @test
* @group structure
*/
public function allDefinedTargetsShouldHaveADescriptionAttribute() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$xpath = "//target";
$targetElements = $xml->xpath($xpath);
$describedTargetElements = array();
foreach ($targetElements as $index => $targetElement) {
$targetDescription = trim($targetElement->attributes()->description);
if ($targetDescription !== '') {
$describedTargetElements[] = $targetDescription;
}
}
$this->assertEquals(count($targetElements),
count($describedTargetElements),
'Description not for all targets set'
);
}
/**
* @test
* @group structure
*/
public function githubCloneAdhocTaskShouldBeDefined() {
$xml = new SimpleXMLElement($this->_buildfileXml);
$xpath = "//target[@name='init-ad-hoc-tasks']/adhoc-task";
$adhocElement = $xml->xpath($xpath);
$this->assertSame('github-clone',
trim($adhocElement[0]->attributes()->name),
"Ad hoc task 'github-clone' isn't defined"
);
}
/**
* @test
* @group artifact
*/
public function initTargetShouldCreateInitialBuildArtifacts() {
$this->_isTearDownNecessary = true;
$this->_buildfileRunner->runTarget(array('init'));
$expectedInitArtifacts = array(
"{$this->_buildfileBasedir}/build",
"{$this->_buildfileBasedir}/build/logs/performance/",
"{$this->_buildfileBasedir}/build/doc",
"{$this->_buildfileBasedir}/build/reports"
);
foreach ($expectedInitArtifacts as $artifact) {
$this->assertFileExists($artifact,
"Expected file '{$artifact}' doesn't exist"
);
}
}
/**
* @test
* @group artifact
*/
public function sqlFilesForDatabaseSetupTargetShouldBeAvailable() {
$expectedSqlFiles = array(
"{$this->_buildfileBasedir}/sqlfiles",
"{$this->_buildfileBasedir}/sqlfiles/session-storage.sql",
"{$this->_buildfileBasedir}/sqlfiles/acl.sql",
"{$this->_buildfileBasedir}/sqlfiles/log.sql"
);
foreach ($expectedSqlFiles as $sqlFile) {
$this->assertFileExists($sqlFile,
"SQL file '{$sqlFile}' doesn't exist"
);
}
}
/**
* @test
* @group artifact
*/
public function githubTargetShouldFetchExpectedRepository() {
$this->_isTearDownNecessary = true;
$this->_buildfileRunner->runTarget(array('github'));
$expectedGitRepository = "{$this->_buildfileBasedir}/build/"
. "github-repos/phploc-phing/.git";
$this->assertFileExists($expectedGitRepository,
"Github target doesn't fetch the expected 'phploc-phing' repository"
);
}
}</pre>The outro screenshot shows the above stated test class run against the example buildfile on a Mac OS X system utilizing the --colors option; which by the way comes in really handy in combination with <a href="http://mikenaberezny.com/2007/09/04/faster-tdd-with-stakeout-rb/" target="_self">Stakeout.rb</a> during the process of refactoring or extending/creating buildfiles the test-driven way.<br />
<br />
<a href="http://www.flickr.com/photos/raphaelstolt/3517905691/" title="PHPUnit console output by Raphael Stolt, on Flickr"><img alt="PHPUnit console output" border="0" height="199" src="http://farm4.static.flickr.com/3559/3517905691_f044985a04_o.gif" width="550" /></a>Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-46777813342974081812009-04-18T02:48:00.006+02:002009-04-18T18:34:11.111+02:00Creating and using Phing ad hoc tasksSometimes there are build scenarios where you'll badly need a functionality, like adding a <a href="http://en.wikipedia.org/wiki/MD5#Applications" target="_self">MD5</a> checksum file to a given project, that isn't provided neither by the available Phing core nor the optional tasks. Phing supports developers with two ways for extending the useable task pool: by writing 'outline' tasks that will end up in a directory of the Phing installation or by utilizing the <a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#AdhocTaskdefTask" target="_self">AdhocTaskdefTask</a>, which allows to define custom tasks in the buildfile itself. The following post will try to outline how to define and use these inline tasks, by sketching an ad hoc task that enables the build orchestra to clone Git repositories from GitHub during a hypothetical workbench setup.<br /><h4 class="custom">Creating the inline/ad hoc task</h4>The AdhocTaskdefTask expects a name attribute i.e. <em>github-clone</em> for the XML element which will later referr to the ad hoc task and a CDATA section hosting the task implementation. Similar to 'outline' tasks the ad hoc task extends Phing's Task class, configures the task via attributes and holds the logic to perform. Unfortunately inline task implementations don't allow to require or include external classes available in the <em>include_path</em>, like <a href="http://framework.zend.com/manual/en/zend.http.html" target="_self">Zend_Http_Client</a> which I initially tried to use for an example task fetching short Urls from is.gd. This limits the available functions and classes to craft the task from to the ones built into PHP. The following buildfile snippet shows the implementation of the github-clone ad hoc task which is wrapped by a <a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self">private target</a> to encourage reusability and limit it's <a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self">callability</a>.<pre class="xmlSnippet"><target name="-init-ad-hoc-tasks" <br /> description="Initializes the ad hoc task(s)"><br /> <adhoc-task name="github-clone"><![CDATA[<br /> class Github_Clone extends Task {<br /><br /> private $repository = null;<br /> private $destDirectory = null;<br /><br /> function setRepos($repository) {<br /> $this->repository = $repository;<br /> } <br /> function setDest($destDirectory) {<br /> $this->destDirectory = $destDirectory;<br /> }<br /> function main() {<br /> // Get project name from repos Uri<br /> $projectName = str_replace('.git', '', <br /> substr(strrchr($this->repository, '/'), 1));<br /><br /> $gitCommand = 'git clone ' . $this->repository . ' ' . <br /> $this->destDirectory . '/' . $projectName;<br /><br /> exec(escapeshellcmd($gitCommand), $output, $return);<br /><br /> if ($return !== 0) {<br /> throw new BuildException('Git clone failed');<br /> }<br /> $logMessage = 'Cloned Git repository ' . $this->repository . <br /> ' into ' . $this->destDirectory . '/' . $projectName;<br /> $this->log($logMessage);<br /> }<br /> }<br /> ]]></adhoc-task><br /> <echo message="Initialized github-clone ad hoc task." /><br /></target></pre><h4 class="custom">Using the ad hoc task</h4>With the ad hoc task in the place to be, it's provided functionality can now be used from any target using the tasks XML element according to the given name i.e. <em>github-clone</em> in the AdhocTaskdefTask element earlier and by feeding it with the required attributes i.e. <em>repos</em> and <em>dest</em>. The next snippet allows you to take a peek at the complete buildfile with the ad hoc task in action.<pre class="xmlSnippet"><?xml version="1.0" encoding="UTF-8"?><br /><project name="recordshelf" default="init-work-bench" basedir="."><br /> <br /> <property name="github.repos.dir" value="./github-repos" override="true" /><br /><br /> <target name="init-work-bench" <br /> depends="-init-ad-hoc-tasks, -clone-git-repos" <br /> description="Initializes the hypothetical workbench"><br /> <echo message="Initialized workbench." /><br /> </target><br /><br /> <target name="-clean-git-repos" <br /> description="Removes old repositories before initializing a new workbench"><br /> <delete dir="${github.repos.dir}" includeemptydirs="true" failonerror="true" /><br /> </target><br /><br /> <target name="-init-ad-hoc-tasks" <br /> description="Initializes the ad hoc task(s)"><br /> <adhoc-task name="github-clone"><![CDATA[<br /> class Github_Clone extends Task {<br /><br /> private $repository = null;<br /> private $destDirectory = null;<br /><br /> function setRepos($repository) {<br /> $this->repository = $repository;<br /> } <br /> function setDest($destDirectory) {<br /> $this->destDirectory = $destDirectory;<br /> }<br /> function main() {<br /> // Get project name from repos Uri<br /> $projectName = str_replace('.git', '', <br /> substr(strrchr($this->repository, '/'), 1));<br /><br /> $gitCommand = 'git clone ' . $this->repository . ' ' . <br /> $this->destDirectory . '/' . $projectName;<br /><br /> exec(escapeshellcmd($gitCommand), $output, $return);<br /><br /> if ($return !== 0) {<br /> throw new BuildException('Git clone failed');<br /> }<br /> $logMessage = 'Cloned Git repository ' . $this->repository . <br /> ' into ' . $this->destDirectory . '/' . $projectName;<br /> $this->log($logMessage);<br /> }<br /> }<br /> ]]></adhoc-task><br /> <echo message="Initialized github-clone ad hoc task." /><br /> </target><br /> <br /> <target name="-clone-git-repos" depends="-clean-git-repos"<br /> description="Clones the needed Git repositories from GitHub"><br /> <github-clone repos="git://github.com/abc/abc.git" <br /> dest="${github.repos.dir}" /><br /> <github-clone repos="git://github.com/xyz/xyz.git" <br /> dest="${github.repos.dir}" /><br /> </target><br /> <br /></project></pre><h4 class="custom">Favouring inline over 'outline' tasks?</h4>The one big advantage of using inline tasks over 'outline' tasks is that they are distributed with the buildfile and are instantly available without the need to modify the Phing installation. Some severe disadvantages of inline tasks are the limitation to use only the core PHP functions and classes for the implementation, the introduction of an additional hurdle to verify the task behaviour via PHPUnit as it's located in a CDATA section of the buildfile and the fact that the use of several inline tasks will blow up the buildfile, and thereby obfuscate the build flow.<br /><br />Regrettably Phing doesn't provide an <a href="http://ant.apache.org/manual/CoreTasks/import.html" target="_self">import</a> task like Ant which might enable a refactoring to pull the ad hoc task definitions into a seperate XML file and include them at buildtime; in case you might have some expertise or ideas for a suitable workaround hit me with a comment. So far I tried to get it working, with no success, by utilizing Phing's <a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#PhingTask">PhingTask</a> and XML's <a href="http://www.w3.org/TR/REC-xml/#sec-external-ent">external entities</a> declaration.<br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com3tag:blogger.com,1999:blog-8420118650236071171.post-19127125686745902572009-03-31T16:02:00.061+02:002009-04-18T18:34:30.816+02:00Using Haml & Sass from a Rake task<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://farm4.static.flickr.com/3007/3401813196_6d7d035d7b_o.gif"><img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 198px; height: 215px;" src="http://farm4.static.flickr.com/3007/3401813196_6d7d035d7b_o.gif" border="0" alt="Haml logo" title="Haml logo" border="0" /></a>Some time ago I had the 'lightning' idea to implement <a href="http://raphaelstolt.blogspot.com/2009/01/broadcasting-blog-post-notifications-to.html" target="_self">another</a> Rake automation to support my current blogging workflow, which at the moment consists of finding a sparkling idea to blog about, write it out in <a href="http://www.hogbaysoftware.com/products/writeroom" target="_self">WriteRoom</a> and refine the post in <a href="http://macromates.com/" target="_self">TextMate</a> before publishing. As this process was a recurring and copy & paste driven event, I strove for an automation supporting this workflow. So unsurprisingly the post will show my current solution to achieve this goal by utilizing <a href="http://rake.rubyforge.org/" target="_self">Rake</a>, <a href="http://haml.hamptoncatlin.com" target="_self">Haml</a> and <a href="http://haml.hamptoncatlin.com/docs/rdoc/classes/Sass.html" target="_self">Sass</a>.<h4 class="custom">So what's that Haml and Sass thingy?</h4>Haml (HTML Abstraction Markup Language) is a templating language/engine with the primary goal to make Markup <a href="http://en.wikipedia.org/wiki/DRY" target="_self">DRY</a>, beautiful and readable again. It has a very shallow learning curve and therefor is perfectly suited for programmers and designers alike. Haml is primarily targeted at making the views of <a href="http://rubyonrails.org/" target="_self">Ruby on Rails</a>, <a href="http://merbivore.com/" target="_self">Merb</a> or <a href="http://www.sinatrarb.com/" target="_self">Sinatra</a> web applications leaner, but as you will see later the Ruby implementation also can be used framework independently.<br /><br />Sass (Syntactically Awesome StyleSheets) is a module which comes bundled with Haml providing a meta-language/abstraction on top of CSS sharing the same goals and advantages as Haml.<h4 class="custom">Gluing Haml and Sass into a Rake task</h4>To get going you first have to install Haml and Sass by running the gem command shown next.<pre class="consoleOutput">sudo gem install haml</pre>With Haml and Sass available it's about time to identify and outline the parts you want to automate, in my case it's the creation of a WriteRoom and/or a XHTML draft document for initial editings. So the parameters to pass into the task to come are the targeted <em>editor(s)</em>, the <em>title</em> of the blog post to draft and a list of associated and whitespace separated <em>category tags</em>.<br /><br />The XHTML document skeleton content and it's inline CSS are defined each in a separate Haml and Sass template file and will be rendered into the outcoming document along with the content passed into the Rake task. While the document skeleton for the WriteRoom draft document, due to it's brevity, is defined inside of the task itself. The following snippets are showing the mentioned Haml and Sass templates for the XHTML draft output file, which are located in the same directory as the Rake file.<br /><br /><div class="refactoringStatus"> Haml</div><pre class="codeSnippetRefactoring">!!! 1.1<br />%html<br /> %head <br /> %title= "<em>#{title}</em> - Draft"<br /> %style{ :type => 'text/css' }= <em>inline_css</em><br /> %body<br /> %h3= <em>title</em><br /> %h4.custom sub headline <br /> %pre.consoleOutput console command<br /> %pre.codeSnippet code snippet<br /> %br/<br /> = "Tags: <em>#{tags.join ', '}</em>"</pre><div class="refactoringStatus"> Sass</div><pre class="codeSnippetRefactoring">body<br /> :margin 5<br /> :line-height 1.5em<br /> :font small Trebuchet MS, Verdana, Arial, Sans-serif<br /> :color #000000<br />h4<br /> :margin-bottom 0.3em<br />.consoleOutput<br /> :padding 6px <br /> :background-color #000 <br /> :color rgb(20, 218, 62)<br /> :font-size 12px<br /> :font-weight bolder<br />.codeSnippet<br /> :padding 3px<br /> :background-color rgb(243, 243, 243)<br /> :color rgb(93, 91, 91)<br /> :font-size small<br /> :border 1px solid #6A6565</pre>To inject the dynamic content into the Haml template and have it rendered into the outcoming document, the values i.e. <em>draft_title</em>, <em>draft_tags</em> and <em>draft_inline_css</em> have to be made available to the template engine by passing them in a bundling Hash into the <em>to_html</em> alias method of the Haml Engine object like shown in the next Rake task.<pre class="codeSnippet">task :default do<br /> Rake::Task['blog_utils:create_draft_doc'].invoke<br />end<br /><br />namespace :blog_utils do<br /> <br /> desc 'Create a new draft document for a given title, category tags and editor'<br /> task :create_draft_doc, [:title, :tags, :editor] do |t, args|<br /> draft_title = args.title<br /> draft_tags = args.tags.split(' ')<br /> draft_target_editor = args.editor<br /> <br /> raise_message = 'No title for draft provided'<br /> raise raise_message if draft_title.nil?<br /> <br /> raise_message = 'No tags for draft provided'<br /> raise raise_message if draft_tags.nil?<br /> <br /> draft_target_editor = '*' if draft_target_editor.nil?<br /> <br /> raise_message = 'Unsupported target editor provided' <br /> raise raise_message unless draft_target_editor == 'Textmate' || <br /> draft_target_editor == 'Writeroom' || draft_target_editor == '*'<br /> <br /> if draft_target_editor == 'Writeroom' || draft_target_editor == '*'<br /> draft_output_file = draft_title.gsub(' ', '_') + '.txt'<br /> <br /> File.open(draft_output_file, 'w') do |draft_file_txt|<br /> draft_file_txt.puts draft_title<br /> draft_file_txt.puts<br /> draft_file_txt.puts "Tags: #{draft_tags.join ', '}"<br /> end<br /> end<br /> <br /> if draft_target_editor == 'Textmate' || draft_target_editor == '*'<br /> <br /> template_sass_content, template_haml_content = ''<br /> <br /> ['haml', 'sass'].each do |template_type|<br /> template = File.dirname(__FILE__) + "/draft_template.#{template_type}"<br /> raise_message = "#{template_type.capitalize} template '#{template}' not found"<br /> raise raise_message if !File.exists?(template)<br /> <br /> template_sass_content = File.read(template) if template_type === 'sass'<br /> template_haml_content = File.read(template) if template_type === 'haml'<br /> end<br /> <em><br /> require 'sass'<br /> require 'haml'<br /><br /> draft_inline_css = Sass::Engine.new(template_sass_content).to_css<br /> draft_document_content = Haml::Engine.new(template_haml_content).to_html(<br /> Object.new, { :title => draft_title , :tags => draft_tags ,<br /> :inline_css => draft_inline_css } )</em><br /> <br /> draft_output_file = draft_title.gsub(' ', '_') + '.html' <br /> File.open(draft_output_file, 'w') do |draft_file_html|<br /> draft_file_html.puts(draft_document_content)<br /> end<br /> end<br /> <br /> end<br />end</pre><h4 class="custom">Easing invocation pain with alias</h4>Now as the Rake task is implemented and waiting for demands it can be invoked by calling the task as shown in the next console snippet. <pre class="consoleOutput">sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc['Title','Tag1 TagN','Editor']</pre>As I'm not even close to being a console ninja and probably will have forgotten the task call structure before initiating the next blog post, I decided to add an easing and more memorizable alias to <em>$HOME/.profile</em> as shown next.<pre class="codeSnippet">alias createdraft='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:create_draft_doc[$title,$tags,$editor]'</pre>The created alias now allows to invoke the Rake task in a nice and easy way as shown in the next console command.<pre class="consoleOutput">createdraft title='Using Haml & Sass from a Rake task' tags='Rake Ruby' editor='Textmate'</pre><h4 class="custom">Taking a peek at the generated draft document</h4>After running the described Rake task I end up with the XHTML document shown in the outro code snippet, which then can be used for the further editing process. Of course I could have setup a <a href="http://raphaelstolt.blogspot.com/2008/02/creating-zend-framework-snippets-for.html" target="_self">TextMate Snippet</a> to get me going, but that way I would have missed the opportunity to mess around with another amazing Ruby tool.<pre class="codeSnippet"><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"><br /><html><br /> <head><br /> <title><em>Using Haml & Sass from a Rake task</em> - Draft</title><br /> <style type='text/css'><br /> <em>body {<br /> margin: 5;<br /> line-height: 1.5em;<br /> font: small Trebuchet MS, Verdana, Arial, Sans-serif;<br /> color: #000000; }<br /><br /> h4 {<br /> margin-bottom: 0.3em; }<br /><br /> .consoleOutput {<br /> padding: 6px;<br /> background-color: #000;<br /> color: rgb(20, 218, 62);<br /> font-size: 12px;<br /> font-weight: bolder; }<br /> <br /> .codeSnippet {<br /> padding: 3px;<br /> background-color: rgb(243, 243, 243);<br /> color: rgb(93, 91, 91);<br /> font-size: small;<br /> border: 1px solid #6A6565; }</em><br /> </style><br /> </head><br /> <body><br /> <h3><em>Using Haml & Sass from a Rake task</em></h3><br /> <h4>sub headline</h4><br /> <pre class='consoleOutput'>console command</pre><br /> <pre class='codeSnippet'>code snippet</pre><br /> <br /><br /> Tags: <em>Rake, Ruby</em><br /> </body><br /></html></pre><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-62060186386088477322009-02-22T14:35:00.007+01:002009-04-18T18:34:51.838+02:00Phplocing your projects with PhingWhen I started to play around with <a href="http://rubyonrails.org/" target="_self">Ruby on Rails</a>, my attention got somehow soon drawn to it's <a href="http://rake.rubyforge.org/" target="_self">Rake</a> stats task, which provides developers or more likely project managers with an overview of the actual project size. Exactly one month ago Sebastian Bergmann, of PHPUnit fame, started to implement a similar tool dubbed <a href="http://github.com/sebastianbergmann/phploc/tree/master" target="_self">phploc</a> which can give you an overview of the size for any given PHP project. As I wanted to automate the invocation of this handy tool and collect it's report output out of a <a href="http://phing.info/trac/" target="_self">Phing</a> buildfile, I invested some time to develop a custom Phing task doing so. Thereby the following post will show you a possible implementation of this task and it's use in a buildfile.<h4 class="custom">Installing phploc</h4>To setup phploc on your system simply install the phploc PEAR package available from the <em>pear.phpunit.de</em> channel as shown in the next commands. In case you already have installed PHPUnit via PEAR you can omit the channel-discover command.<pre class="consoleOutput">sudo pear channel-discover pear.phpunit.de<br />sudo pear install phpunit/phploc</pre><h4 class="custom">Implementing the phploc task</h4>As I already blogged about <a href="http://raphaelstolt.blogspot.com/2007/03/rolling-your-own-phing-task.html" target="_self">developing custom Phing task</a> I'm only going to show the actual implementation and not dive into any details; alternatively you can also grab it from <a href="http://github.com/raphaelstolt/phploc-phing/tree/master" target="_self">this</a> public GitHub repository.<pre class="codeSnippet"><?php<br />require_once 'phing/Task.php';<br />require_once 'phing/BuildException.php';<br />require_once 'PHPLOC/Analyser.php';<br />require_once 'PHPLOC/Util/FilterIterator.php';<br />require_once 'PHPLOC/TextUI/ResultPrinter.php';<br /><br />class PHPLocTask extends Task<br />{<br /> protected $suffixesToCheck = null;<br /> protected $acceptedReportTypes = null;<br /> protected $reportDirectory = null;<br /> protected $reportType = null;<br /> protected $fileToCheck = null;<br /> protected $filesToCheck = null;<br /> protected $reportFileName = null;<br /> protected $fileSets = null;<br /> <br /> public function init() {<br /> $this->suffixesToCheck = array('php');<br /> $this->acceptedReportTypes = array('cli', 'txt', 'xml');<br /> $this->reportType = 'cli';<br /> $this->reportFileName = 'phploc-report';<br /> $this->fileSets = array();<br /> $this->filesToCheck = array();<br /> }<br /> public function setSuffixes($suffixListOrSingleSuffix) {<br /> if (stripos($suffixListOrSingleSuffix, ',')) {<br /> $suffixes = explode(',', $suffixListOrSingleSuffix);<br /> $this->suffixesToCheck = array_map('trim', $suffixes);<br /> } else {<br /> array_push($this->suffixesToCheck, trim($suffixListOrSingleSuffix));<br /> }<br /> }<br /> public function setFile(PhingFile $file) {<br /> $this->fileToCheck = trim($file);<br /> }<br /> public function createFileSet() {<br /> $num = array_push($this->fileSets, new FileSet());<br /> return $this->fileSets[$num - 1];<br /> }<br /> public function setReportType($type) {<br /> $this->reportType = trim($type);<br /> }<br /> public function setReportName($name) {<br /> $this->reportFileName = trim($name);<br /> }<br /> public function setReportDirectory($directory) {<br /> $this->reportDirectory = trim($directory);<br /> }<br /> public function main() { <br /> if (!isset($this->fileToCheck) && count($this->fileSets) === 0) {<br /> $exceptionMessage = "Missing either a nested fileset or the "<br /> . "attribute 'file' set.";<br /> throw new BuildException($exceptionMessage);<br /> }<br /> if (count($this->suffixesToCheck) === 0) {<br /> throw new BuildException("No file suffix defined.");<br /> }<br /> if (is_null($this->reportType)) {<br /> throw new BuildException("No report type defined.");<br /> }<br /> if (!is_null($this->reportType) && <br /> !in_array($this->reportType, $this->acceptedReportTypes)) {<br /> throw new BuildException("Unaccepted report type defined.");<br /> }<br /> if (!is_null($this->fileToCheck) && !file_exists($this->fileToCheck)) {<br /> throw new BuildException("File to check doesn't exist.");<br /> }<br /> if ($this->reportType !== 'cli' && is_null($this->reportDirectory)) {<br /> throw new BuildException("No report output directory defined.");<br /> }<br /> if (count($this->fileSets) > 0 && !is_null($this->fileToCheck)) {<br /> $exceptionMessage = "Either use a nested fileset or 'file' " <br /> . "attribute; not both.";<br /> throw new BuildException($exceptionMessage);<br /> }<br /> if (!is_null($this->reportDirectory) && !is_dir($this->reportDirectory)) {<br /> $reportOutputDir = new PhingFile($this->reportDirectory);<br /> $logMessage = "Report output directory does't exist, creating: " <br /> . $reportOutputDir->getAbsolutePath() . '.';<br /> $this->log($logMessage);<br /> $reportOutputDir->mkdirs();<br /> }<br /> if ($this->reportType !== 'cli') {<br /> $this->reportFileName.= '.' . trim($this->reportType);<br /> }<br /> if (count($this->fileSets) > 0) {<br /> $project = $this->getProject();<br /> foreach ($this->fileSets as $fileSet) {<br /> $directoryScanner = $fileSet->getDirectoryScanner($project);<br /> $files = $directoryScanner->getIncludedFiles();<br /> $directory = $fileSet->getDir($this->project)->getPath();<br /> foreach ($files as $file) {<br /> if ($this->isFileSuffixSet($file)) {<br /> $this->filesToCheck[] = $directory . DIRECTORY_SEPARATOR <br /> . $file;<br /> }<br /> }<br /> }<br /> $this->filesToCheck = array_unique($this->filesToCheck);<br /> }<br /> if (!is_null($this->fileToCheck)) {<br /> if (!$this->isFileSuffixSet($file)) {<br /> $exceptionMessage = "Suffix of file to check is not defined in"<br /> . " 'suffixes' attribute.";<br /> throw new BuildException($exceptionMessage);<br /> }<br /> }<br /> $this->runPhpLocCheck();<br /> }<br /> protected function isFileSuffixSet($filename) {<br /> $pathinfo = pathinfo($filename);<br /> $fileSuffix = $pathinfo['extension'];<br /> return in_array($fileSuffix, $this->suffixesToCheck);<br /> }<br /> protected function runPhpLocCheck() {<br /> $files = $this->getFilesToCheck();<br /> $result = $this->getCountForFiles($files); <br /> <br /> if ($this->reportType === 'cli' || $this->reportType === 'txt') {<br /> $printer = new PHPLOC_TextUI_ResultPrinter;<br /> if ($this->reportType === 'txt') {<br /> ob_start();<br /> $printer->printResult($result);<br /> file_put_contents($this->reportDirectory <br /> . DIRECTORY_SEPARATOR . $this->reportFileName, <br /> ob_get_contents());<br /> ob_end_clean();<br /> $reportDir = new PhingFile($this->reportDirectory);<br /> $logMessage = "Writing report to: " <br /> . $reportDir->getAbsolutePath() . DIRECTORY_SEPARATOR <br /> . $this->reportFileName;<br /> $this->log($logMessage);<br /> } else {<br /> $printer->printResult($result);<br /> }<br /> } elseif ($this->reportType === 'xml') {<br /> $xml = $this->getResultAsXml($result);<br /> $reportDir = new PhingFile($this->reportDirectory);<br /> $logMessage = "Writing report to: " . $reportDir->getAbsolutePath()<br /> . DIRECTORY_SEPARATOR . $this->reportFileName;<br /> $this->log($logMessage);<br /> file_put_contents($this->reportDirectory . DIRECTORY_SEPARATOR<br /> . $this->reportFileName, $xml);<br /> }<br /> }<br /> protected function getFilesToCheck() {<br /> if (count($this->filesToCheck) > 0) {<br /> $files = array();<br /> foreach ($this->filesToCheck as $file) {<br /> $files[] = new SPLFileInfo($file);<br /> }<br /> } elseif (!is_null($this->fileToCheck)) {<br /> $files = array(new SPLFileInfo($this->fileToCheck));<br /> }<br /> return $files;<br /> }<br /> protected function getCountForFiles($files) {<br /> $count = array('files' => 0, 'loc' => 0, 'cloc' => 0, 'ncloc' => 0,<br /> 'eloc' => 0, 'interfaces' => 0, 'classes' => 0, 'functions' => 0);<br /> $directories = array();<br /><br /> foreach ($files as $file) {<br /> $directory = $file->getPath();<br /> if (!isset($directories[$directory])) {<br /> $directories[$directory] = TRUE;<br /> } <br /> PHPLOC_Analyser::countFile($file->getPathName(), $count);<br /> }<br /> <br /> if (!function_exists('parsekit_compile_file')) {<br /> unset($count['eloc']);<br /> }<br /> $count['directories'] = count($directories) - 1;<br /> return $count;<br /> }<br /> protected function getResultAsXml($result) { <br /> $newline = "\n";<br /> $newlineWithSpaces = sprintf("\n%4s",'');<br /> $xml = '<?xml version="1.0" encoding="UTF-8"?>';<br /> $xml.= $newline . '<phploc>'; <br /> <br /> if ($result['directories'] > 0) {<br /> $xml.= $newlineWithSpaces . '<directories>' . $result['directories'] . '</directories>';<br /> $xml.= $newlineWithSpaces . '<files>' . $result['files'] . '</files>';<br /> }<br /> $xml.= $newlineWithSpaces . '<loc>' . $result['loc'] . '</loc>';<br /> <br /> if (isset($result['eloc'])) {<br /> $xml.= $newlineWithSpaces . '<eloc>' . $result['eloc'] . '</eloc>';<br /> }<br /> $xml.= $newlineWithSpaces . '<cloc>' . $result['cloc'] . '</cloc>';<br /> $xml.= $newlineWithSpaces . '<ncloc>' . $result['ncloc'] . '</ncloc>';<br /> $xml.= $newlineWithSpaces . '<interfaces>' . $result['interfaces'] . '</interfaces>';<br /> $xml.= $newlineWithSpaces . '<classes>' . $result['classes'] . '</classes>';<br /> $xml.= $newlineWithSpaces . '<methods>' . $result['functions'] . '</methods>' . $newline;<br /> $xml.= '</phploc>';<br /> return $xml;<br /> }<br />}</pre><h4 class="custom">Hooking the phploc task into Phing</h4>To use the task in your Phing builds simply copy it into the <em>phing/tasks/my</em> directory and make it available via the <a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#TaskdefTask" target="_self">taskdef task</a>. The next table shows the available task attributes and the values they can take to configure it's behaviour and output. As you will see it also provides the ability to generate reports in a XML format; I chose to implement this feature to have the possibilty to transform the report results into HTML documents by applying for example a XSLT stylesheet. This way they can provide more value to non-technical project members or can be made accessible in a CI system dashboard if desired.<br /><br /><table class="listing" cellpadding="1"cellspacing="1"><tbody><tr><th>Name</th><th>Type</th><th>Description</th><th>Default</th><th>Required</th></tr><tr><td class="content" valign="top">reportType</td><td class="content" valign="top">string</td><td class="content" valign="top">The type of the report. Available types are cli|txt|xml.</td><td class="content" valign="top">cli</td><td class="content" valign="top">No</td></tr><tr><td class="content" valign="top">reportName</td><td class="content" valign="top">string</td><td class="content" valign="top">The name of the report type without a file extension.</td><td class="content" valign="top">phploc-report</td><td class="content" valign="top">No</td></tr><tr><td class="content" valign="top">reportDirectory</td><td class="content" valign="top">string</td><td class="content" valign="top">The directory to write the report file to.</td><td class="content" valign="top">false</td><td class="content" valign="top">Yes, when report type txt or xml is defined.</td></tr><tr><td class="content" valign="top">file</td><td class="content" valign="top">string</td><td class="content" valign="top">The name of the file to check.</td><td class="content" valign="top">n/a</td><td class="content" valign="top">Yes, when no nested fileset is defined.</td></tr><tr><td class="content" valign="top">suffixes</td><td class="content" valign="top">string</td><td class="content" valign="top">A comma-separated list of file suffixes to check.</td><td class="content" valign="top">php</td><td class="content" valign="top">No</td></tr></tbody></table><br /><span style="font-weight:bold;">Supported Nested Tags:</span><br /><ul type="square" style="padding-left: 25px; line-height: 15px;"><li>fileset</li></ul>The closing buildfile extract shows an example phploc task configuration and is also available at the <a href="http://github.com/raphaelstolt/phploc-phing/tree/master" target="_self">public GitHub repository</a>. Happy phplocing!<pre class="xmlSnippet"><?xml version="1.0"?><br /><project name="example" default="phploc" basedir="."><br /> <taskdef name="phploc" classname="phing.tasks.my.PHPLocTask" /><br /> <target name="phploc"><br /> <tstamp><br /> <format property="check.date.time" pattern="%Y%m%d-%H%M%S" locale="en_US"/><br /> </tstamp><br /> <phploc reportType="txt" reportName="${check.date.time}-report"<br /> reportDirectory="phploc-reports"><br /> <fileset dir="."><br /> <include name="**/*.php" /><br /> <include name="*.php" /><br /> </fileset><br /> </phploc><br /> </target><br /></project></pre><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com2tag:blogger.com,1999:blog-8420118650236071171.post-88479832963304237692009-01-24T06:08:00.032+01:002009-05-10T16:36:07.642+02:00Broadcasting blog post notifications to Twitter with Ruby and Rake<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.flickr.com/photos/raphaelstolt/3228209607/" title="Blogger to Twitter Logo by Raphael Stolt, on Flickr"><img src="http://farm4.static.flickr.com/3421/3228209607_f9e31cfb5b_m.jpg" style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 240px; height: 103px;" width="240" height="103" border="0" alt="Blogger to Twitter Logo" /></a>During my latest blogging absence I had some time to tinker around with Ruby. For an introductory challenge I chose to implement a real life feature which currently isn't supported by Blogger.com and screams siren-like for an one-button automation: Broadcasting the latest blog entry to my Twitter account. As I didn't want to sign up for a <a href="http://twitterfeed.com/" target="_self">Twitterfeed</a> account and couldn't resort to the <a href="http://alexking.org/projects/wordpress" target="_self">Twitter Tools</a> plugin like WordPress users, I had to perform these broadcasting steps manually, until now. To see how this repetitive and time-stealing process was transformed into a semi-automated one by utilizing Ruby, a splash of <a href="http://code.whytheluckystiff.net/hpricot/" target="_self">Hpricot</a>, Ruby's excellent <a href="http://twitter.rubyforge.org/rdoc/" target="_self">Twitter Api wrapper</a> and <a href="http://rake.rubyforge.org/" target="_self">Rake</a>, read on my dear.<h4 class="custom">Installing the required RubyGems</h4>Prior to diving into the implementation details of the given scenario I had to install the required <a href="http://www.rubygems.org/" target="_self">RubyGems</a> like shown in the next console snippet. The installation of the twitter gem might take a while due to it's dependency on several other gems.<pre class="consoleOutput">sudo gem install hpricot rake twitter</pre><h4 class="custom">Scraping the latest blog post details with Hpricot</h4>The initial implementation step was to gather relevant metadata (Url, title and used tags) of the latest blog post. I first took the route to get it by grabbing the blog's RSS feed and extracting the metadata from there, but soon stumbled into problems getting an outdated feed from Feedburner. The next alternative was to scrape the needed metadata directly from the blog landing page. As I went this route <a href="http://raphaelstolt.blogspot.com/2008/10/scraping-websites-with-zenddomquery.html" target="_self">before</a> with the Zend_Dom_Query component of the Zend Framework I decided to use something similar from the Ruby toolbox. Some Google hops later I was sold to Hpricot, a HTML Parser for Ruby and as you can see in the first code snippet, showing an extract of the Rake file to come, this is done in just 13 lines of code.<pre class="codeSnippet">doc = Hpricot(open(blog_landing_page, scrape_options))<br />latest_post_url = doc.at('h3.post-title > a')['href']<br />latest_post_title = doc.at('h3.post-title > a').inner_html<br />label_doc = Hpricot(doc.search('span.post-labels').first.to_s)<br />label_links = label_doc.search('span.post-labels > a').each do |label_link|<br /> label = label_link.inner_html.gsub(' ', '').downcase<br /> if label.include?('/')<br /> labels = label.split('/')<br /> labels.each { |label| last_post_labels.push(label) }<br /> else<br /> last_post_labels.push(label)<br /> end<br />end</pre><h4 class="custom">Outstanding tasks</h4>With the metadata available the oustanding tasks to implement were:<ul type="square" style="padding-left: 25px; line-height: 15px;"><li>to get a short Url for the actual blog post by utilzing a public API of an Url shortening service i.e. <a href="http://is.gd/" target="_self">is.gd</a></li><li>to build the tweet to broadcast by injecting the available metadata into a tweet template</li><li>to broadcast the notification tweet to the given Twitter account</li><li>to log the broadcasted blog title to prevent spamming or duplication scenarios</li></ul>As a guy sold to build tools and eager to learn something new I subverted Rake, Ruby's number one build language, to glue the above mentioned tasks and their implementation together, to manage their sequential dependencies and to have a comfortable invocation interface. The nice thing about Rake is that it allows you to implement each tasks unit of work by using the Ruby language; and there is no need to follow a given structure to implement custom tasks like it's the case for <a href="http://raphaelstolt.blogspot.com/2007/03/rolling-your-own-phing-task.html" target="_self">custom Phing tasks</a>. As you will see in the forthcoming complete Rakefile some of the tasks are getting quite long and complex; therefor some of them are pending candidates for Refactoring activities like for example extract task units of work into helper/worker classes.<pre class="codeSnippet"> require 'rubygems'<br /> require 'hpricot'<br /> require 'open-uri'<br /> require 'twitter'<br /><br /> task :default do<br /> Rake::Task['blog_utils:broadcast_notification'].invoke<br /> end<br /><br /> namespace :blog_utils do<br /><br /> scrape_options = { 'UserAgent' => "Ruby/#{RUBY_VERSION}" }<br /> blog_landing_page = 'http://raphaelstolt.blogspot.com'<br /> latest_post_short_url, latest_post_url, latest_post_title = nil<br /> notification_tweet = nil<br /> last_post_labels = []<br /> broadcast_log_file = File.dirname(__FILE__) + '/broadcasted_posts.log'<br /> twitter_credentials = { :user => 'raphaelstolt', :pwd => 'thatsasecret'}<br /><br /> desc 'Scrape metadata of latest blog post from landing page'<br /> task :scrape_actual_post_metadata do<br /> doc = Hpricot(open(blog_landing_page, scrape_options))<br /> latest_post_url = doc.at('h3.post-title > a')['href']<br /> latest_post_title = doc.at('h3.post-title > a').inner_html<br /> label_doc = Hpricot(doc.search('span.post-labels').first.to_s)<br /> label_links = label_doc.search('span.post-labels > a').each do |label_link|<br /> label = label_link.inner_html.gsub(' ', '').downcase<br /> if label.include?('/')<br /> labels = label.split('/')<br /> labels.each { |label| last_post_labels.push(label) }<br /> else<br /> last_post_labels.push(label)<br /> end<br /> end<br /> end<br /><br /> desc 'Shorten the Url of the latest blog post'<br /> task :shorten_post_url => [:scrape_actual_post_metadata] do<br /> raise_message = 'No Url for latest blog post available'<br /> raise raise_message if latest_post_url.nil?<br /> url_shorten_service_call = "http://is.gd/api.php?longurl=#{latest_post_url}"<br /> latest_post_short_url = open(url_shorten_service_call, scrape_options).read<br /> end<br /><br /> desc 'Check if generate shorten Url references the latest blog post url'<br /> task :check_shorten_url_references_latest do<br /> url_referenced_by_short_url = nil<br /> open(latest_post_short_url, scrape_options) do |f|<br /> url_referenced_by_short_url = f.base_uri.to_s<br /> end <br /> raise_message = "Generated short Url '#{latest_post_short_url}' does not"<br /> raise_message << " reference actual blog post url '#{latest_post_url}'"<br /> raise raise_message unless url_referenced_by_short_url.eql?(latest_post_url)<br /> end<br /><br /> desc 'Check if latest blog post has already been broadcasted'<br /> task :check_logged_broadcasts do<br /> logged_broadcasts = []<br /> if File.exist?(broadcast_log_file)<br /> File.open(broadcast_log_file, 'r') do |f|<br /> logged_broadcasts = f.readlines.collect { |line| line.chomp }<br /> end<br /> end<br /> raise_message = "Blog post '#{latest_post_title}' has already been "<br /> raise_message << "broadcasted"<br /> raise raise_message if logged_broadcasts.include?(latest_post_title)<br /> end<br /><br /> desc 'Build notification tweet by injecting scraped metadata into template'<br /> task :build_notification_tweet => [:shorten_post_url, <br /> :check_shorten_url_references_latest] do<br /> raise_message = 'Required metadata to build tweet is not available'<br /> raise raise_message if latest_post_title.nil? || latest_post_short_url.nil? <br /> raise raise_message if last_post_labels.nil?<br /><br /> notification_tweet = "Published a new blog post '#{latest_post_title}' "<br /> notification_tweet << "available at #{latest_post_short_url}."<br /><br /> raise_message = 'Broadcast for latest blog post exceeds 140 characters'<br /> raise raise_message if notification_tweet.length > 140<br /><br /> last_post_labels.each do |tag|<br /> notification_tweet << " ##{tag}" unless notification_tweet.length + <br /> " ##{tag}".length > 140<br /> end<br /> end<br /><br /> desc 'Broadcast latest blog post notification to twitter'<br /> task :broadcast_notification_to_twitter => [:build_notification_tweet, <br /> :check_logged_broadcasts] do<br /> raise_message = "Notification tweet to broadcast is not available"<br /> raise raise_message if notification_tweet.nil?<br /> puts "Broadcasting '#{notification_tweet}'"<br /> http_auth = Twitter::HTTPAuth.new(twitter_credentials[:user], twitter_credentials[:pwd])<br /> Twitter::Base.new(http_auth).update(notification_tweet)<br /> #Twitter::Base.new(twitter_credentials[:user], twitter_credentials[:pwd]).post(notification_tweet)<br /> Rake::Task['blog_utils:log_broadcast_title'].invoke<br /> end<br /><br /> desc 'Log broadcasted blog post title'<br /> task :log_broadcast_title do<br /> puts "Logging latest post title to #{broadcast_log_file}"<br /> File.open(broadcast_log_file, 'a') do |f|<br /> f.puts latest_post_title<br /> end<br /> end<br /><br /> end</pre><h4 class="custom">Putting the Rake task(s) to work</h4>The next step was to put the Rakefile into my $HOME directory; and after publishing a new blog post I'm now able to broadcast an automated notification by firing up the console and calling the Rake task like shown next.<pre class="consoleOutput">sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification</pre>And as I'm too lazy to type this lengthy command everytime I further added an alias to the $HOME/.profile file which allows me to call the task via the associated alias i.e. blogger2twitter shown in the .profile excerpt.<pre class="codeSnippet">alias blogger2twitter='sudo rake -f $HOME/Automations/Rakefile.rb blog_utils:broadcast_notification'</pre>After running the Rake task against this blog post the notification gets added to the given Twitter timeline like shown in the outro image.<br /><br /><a href="http://www.flickr.com/photos/raphaelstolt/3261723626/" title="Notification tweet by Raphael Stolt, on Flickr"><img src="http://farm4.static.flickr.com/3434/3261723626_56516e74f4_o.gif" width="523" height="89" border="0" alt="Notification tweet screenshot" /></a><br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com2tag:blogger.com,1999:blog-8420118650236071171.post-82248993447278218932009-01-23T14:19:00.032+01:002009-01-24T03:08:35.047+01:00Installing Zend_Tool on Mac OS XYesterday I decided to tiptoe into the development of custom Zend_Tool Providers as the introductional <a href="http://devzone.zend.com/article/4124-Zend_Tool-for-the-Developer" target="_self">article</a> series by Ralph Schindler motivated me to learn more about it and I already have some useful use cases on my mind. Therefor I prior had to install the Zend_Tool component and it's driving CLI scripts on my MacBook. The following brief instruction describes a possible approach that got me running in no time on a Mac OS X system. Once the Zend Framework has an official PEAR channel most of the forthcoming steps should be obsolete and entirely performed by the PEAR package installer command.<h4 class="custom">Fetching and installing the Zend_Tool component</h4>First I tried to install the 1.8.0(devel) version of the Zend Framework via the <a href="http://pear.zfcampus.org/" target="_self">pear.zfcampus.org</a> PEAR channel but it currently only delivers the 1.7.3PL1(stable) package; even after switching the stability state of the PEAR config. To dodge the include_path setting hassle and for a further use when customizing other tools like Phing tasks I decided to keep the installed package.<pre class="consoleOutput">sudo pear channel-discover pear.zfcampus.org<br />sudo pear install zfcampus/zf-devel</pre>The next commands are showing the footwork I had to do to get the Zend_Tool component into the PEAR Zend Framework package installed in <em>/opt/local/lib/php/Zend</em>.<pre class="consoleOutput">sudo svn co http://framework.zend.com/svn/framework/standard/incubator/library/Zend/Tool/ $HOME/Cos/Zend/Tool<br />sudo rsync -r --exclude=.svn $HOME/Cos/Zend/Tool /opt/local/lib/php/Zend</pre><h4 class="custom">Putting the Zend_Tool CLI scripts to work</h4>The next steps were to fetch the CLI scripts from the public Subversion repository and to link them into the system path <em>/opt/local/bin</em> as shown in the next commands. <pre class="consoleOutput">sudo svn co http://framework.zend.com/svn/framework/standard/incubator/bin $HOME/Cos/Zend/bin<br />sudo ln $HOME/Cos/Zend/bin/zf.sh /opt/local/bin/zf<br />sudo ln $HOME/Cos/Zend/bin/zf.php /opt/local/bin/zf.php</pre><h4 class="custom">Checking the installation</h4>With everything hopefully in place it was time to verify the success of the installation via the below stated provider action call; and as I got the version of the installed Zend Framework as a response of the executed action/command I'm good to go.<pre class="consoleOutput">zf show version</pre><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com0tag:blogger.com,1999:blog-8420118650236071171.post-41757987134212718262008-11-13T16:21:00.007+01:002008-12-08T17:29:20.088+01:00Rails for PHP Developers book review<img style="margin: 0pt 10px 10px 0pt; float: left; cursor: pointer; width: 190px; height: 228px;" src="http://farm3.static.flickr.com/2151/2274548280_0d6516d8a4_o.jpg" alt="Rails for PHP Developers" title="Rails for PHP Developers" border="0" />The e-book version of the Pragmatic Programmers release <a href="http://www.pragprog.com/titles/ndphpr/rails-for-php-developers" target="_self">Rails for PHP Developers</a> written by Derek DeVries and Mike Naberezny occupies now some of my scarce hard drive space for several months, and today I managed to hit the last page of it. In case you're interested in knowing if it's worthy to sacrifice some rare hard drive or bookshelf space for this book read on.<h4 class="custom">What's in it?</h4>The book consists of three main parts which are addressing open-minded developers with a PHP background tempted to add the Ruby language and the thereupon built Rails 2.0 framework to their toolset. <br /><br />The first part introduces the classic and nowadays omnipresent MVC pattern, the concepts and conventions of Rails by converting a simple PHP newsletter application into a Rails based one. The follow-up chapters of the first part are covering the basics of the Ruby language by looking at known PHP language features and constructs, and how they translate to their Ruby counterparts. Reading these chapters you will get a thorough understanding of the Ruby language and be able to apply unique features like blocks or the reopening of existing classes. The communicated knowledge builds the foundation to accelerate the use and understanding of the Rails framework which is covered in-depth through-out the book's second part. <br /><br />While teaming up with their imaginary buddy Joe the authors walk you through building a Rails user group application. The chapters of the second part are covering a lot of ground reaching from domain modeling, putting the particular MVC parts to work, ensuring quality by utilizing the Test::Unit library to finally deploying the application into a <strike>productive</strike> production environment. <br /><br />The first two chapters of the final and reference part cover the differences and similarities between PHP and Ruby data structures, operations and language constructs. The final chapter of the book closes with a web development specific comparision of PHP constructs and approaches to the ones used by the Rails framework. The book is accompanied by a dedicated <a href="http://railsforphp.com/">blog</a> and a <a href="http://railsforphp.com/reference" target="_self">PHP to Rails online reference</a> to satisfy severe thirst for more knowledge.<h4 class="custom">Conclusion</h4>The book provides interested PHP developers a thorough introduction to the Ruby language and the Rails framework in a fluent and enjoyable writing tone. By implementing the example application of the second book part any decent PHP developer will derive a solid understanding of the Rails framework, he can build upon and that puts him in the position to make reasonable judgments for using/flaming it or not. IMHO this book is so far one of the best PHP related book releases of the out fading year 2008, and can be a real motivator to extend the just gained knowledge by diving deeper into the Ruby/Rails ocean. <br /><br />So be prepared to see one or another Ruby related post popping up in the future timeline of this blog; I just added another costly addiction to the medicine cupboard. Word!<br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com2tag:blogger.com,1999:blog-8420118650236071171.post-31176379929150264952008-10-31T22:44:00.002+01:002008-10-31T23:00:54.279+01:00Tinyizing URLs with Zend_Http_ClientWhile doing some initial research for a blog related automation task to implement I learned some more about services which transform long URLs into short ones. The well-knownst of these services, due to the Twitter hype, is probably <a href="http://tinyurl.com/" target="_self">TinyURL</a> which can be accessed via a classic webinterface or by calling a public API. In a recent <a href="http://www.davedevelopment.co.uk/2008/10/13/zend-framework-and-the-twitter-api/" target="_self">blog post</a> Dave Marshall outlined a quick workaround for tweeting via the <a href="http://framework.zend.com/manual/en/zend.http.html#zend.http.client" target="_self">Zend_Http_Client</a> component which is a reasonable approach for calling services that aren't in the Zend Framework core yet like <a href="http://framework.zend.com/wiki/display/ZFPROP/Zend_Service_Twitter" target="_self">Zend_Service_Twitter</a> or are not supported out of the box. Therefore this post will try to describe a Zend Framework way of creating tinyized URLs.<h4 class="custom">Getting tiny tiny y'all</h4>According to Wikipedia there are numerous services available e.g. <a href="http://www.rubyurl.com/home" target="_self">RubyUrl</a> providing the same feature as TinyURL, so to be prepared for the future and thereby maybe violating the <a href="http://en.wikipedia.org/wiki/YAGNI" target="_self">YAGNI principle</a> I decided to declare a very basic interface first in case of switching the service provider someday.<pre class="codeSnippet"><?php<br />/** <br /> * 'Interface-level' PHPDoc Block <br /> */<br />interface Recordshelf_Service_UrlShortener_Interface<br />{<br /> public function __construct($serviceEndpoint = '');<br /> public function shortenize($url);<br />}</pre>The next code snippet shows the implementation for the TinyURL service programmed against the interface and hosting an additional alias method called <em>tinyize</em> which is simply wrapping the actual worker method. The service utilizes Zend_Http_Client by setting the endpoint of the service, transmitting a GET request parameterized with the URL to shorten against it and returning the response containing the tinyized URL.<pre class="codeSnippet"><?php<br />require_once('Zend/Http/Client.php');<br />require_once('Recordshelf/Service/UrlShortener/Interface.php');<br />/** <br /> * 'Class-level' PHPDoc Block <br /> */<br />class Recordshelf_Service_TinyUrl implements<br /> Recordshelf_Service_UrlShortener_Interface<br />{<br /> /**<br /> * The service endpoint<br /> *<br /> * @var string<br /> */<br /> private $_serviceEndpoint = null;<br /> /**<br /> * Recordshelf service tinyURL constructor<br /> *<br /> * @param string $serviceEndpoint<br /> */<br /> public function __construct(<br /> $serviceEndpoint = 'http://tinyurl.com/api-create.php')<br /> {<br /> $this->_serviceEndpoint = $serviceEndpoint;<br /> }<br /> /**<br /> * Shortenizes a given Url<br /> *<br /> * @param string $url<br /> * @return string<br /> * @throws Exception<br /> */<br /> public function shortenize($url) {<br /> if (is_null($this->_serviceEndpoint)) {<br /> throw new Exception('No service endpoint set');<br /> }<br /> $client = new Zend_Http_Client($this->_serviceEndpoint);<br /> $client->setParameterGet('url', $url)<br /> ->setMethod(Zend_Http_Client::GET);<br /> try {<br /> $response = $client->request();<br /> } catch (Exception $e) {<br /> throw $e;<br /> }<br /><br /> if (200 === $response->getStatus()) {<br /> return $response->getBody();<br /> } else {<br /> throw new Exception($response->getStatus() . ": " .<br /> $response->getMessage());<br /> }<br /> }<br /> /**<br /> * Alias method for the shortenize method<br /> *<br /> * @param string $url<br /> * @throws Exception<br /> * @see shortenize<br /> */<br /> public function tinyize($url)<br /> {<br /> return $this->shortenize($url);<br /> }<br />}</pre>Now with everything hopefully operating smoothly it's time for a test-drive, yeah I'm lazy and cut that development approach called TDD, by creating a service instance and requesting a TinyURL for the Zend Framework website as shown in the outro listing.<pre class="codeSnippet"><?php<br />$service = new Recordshelf_Service_TinyUrl();<br />$service->tinyize('http://framework.zend.com');<br />// => http://tinyurl.com/nf8kf</pre>In case off considering or favouring a more framework independent approach there are also other blends available like one via <a href="http://snippets.dzone.com/posts/show/4720" target="_self">file_get_contents</a> or via <a href="http://strategicv.com/2008/09/29/code-snippet-tinyurl-link-creation-api/" target="_self">curl</a>. Happy tinyizing!<br /><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com6tag:blogger.com,1999:blog-8420118650236071171.post-50451551338115601112008-10-26T20:28:00.004+01:002008-10-26T20:45:39.021+01:00Getting a visualization of a Phing buildfileToday I spent some time to get a tool running to visualize Phing buildfiles as this can come in handy for maintaing, <a href="http://raphaelstolt.blogspot.com/2008/07/six-valuable-phing-build-file.html" target="_self">refactoring</a> or extending large buildfiles. Out of the box the Phing <em>-l</em> option can be used to <a href="http://raphaelstolt.blogspot.com/2008/03/getting-overview-of-all-targets.html" target="_self">get a first overview of all available targets in a given buildfile</a> but it doesn't untangle the target dependencies and sometimes a picture is still worth a thousand words. Luckily the Ant community already provides several tools to accomplish the visualization of Ant buildfiles, reaching from solutions that apply a Xslt stylesheet upon a given buildfile e.g. <a href="http://ant2dot.sourceforge.net/" target="_self">ant2dot</a> to those ones that take a programmatically approach e.g. <a href="http://www.ggtools.net/grand/" target="_self">Grand</a>. All these solutions utilize <a href="http://www.graphviz.org/" target="_self">Graphiz</a> to generate a graphic from a DOT file representing the buildfile structure, it's targets and their dependencies. As Phing is a very close descendant of Ant the Xslt approach was best suited and the one with the least effort because their buildfile markup is very similar. The following post will walk you through on how to get a simple Phing buildfile visualization tool running in just a few minutes.<h4 class="custom">Grabbing the Xslt file</h4>The first step is to get the ant2dot Xslt <a href="http://ant2dot.sourceforge.net/xsl/ant2dot.xsl" target="_self">stylesheet</a> and put it into the same directory as the visualization buildfile and target to come. Due to the aforementioned Phing and Ant buildfile markup similarities it can be used without any modfications.<h4 class="custom">Setting up the buildfile visualization target</h4>The next step is to create a Phing target that utilizes the <a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#XsltTask" target="_self">Xslt task</a> to transfrom the fed buildfile into a DOT file which gets passed further to a platform dependent <a href="http://phing.info/docs/guide/current/chapters/appendixes/AppendixB-CoreTasks.html#ExecTask" target="_self">Exec task</a> handling the final transformation into a PNG image. To make the visualization target independent from the buildfile to visualize it's hosted in an own buildfile and the target accepts the buildfile to be transformed as a property passed to the Phing Cli or if none given uses the default build.xml. Further the Xslt stylesheet accepts several parameters to add extended data to the resulting DOT file/PNG image which can be set in the <param> tags of the Xslt task. For a list of possible parameters have a look at the <a href="http://ant2dot.sourceforge.net/#options" target="_self">options</a> section of ant2dot. The following codesnippet shows the visualization buildfile and the <em>visualize</em> target doing the Whodini like magic.<pre class="xmlSnippet"><?xml version="1.0"?><br /><br /><project name="buildfile-visualizer" default="visualize" basedir="."> <br /><br /> <target name="visualize" <br /> description="Generates a visualization(PNG image) of a given buildfile"><br /> <property name="buildfile" value="build.xml" /><br /> <property name="phing2dot.xsl" value="${project.basedir}/ant2dot.xsl" /><br /> <property name="dot.file" value="${buildfile}.dot" /><br /> <property name="png.file" value="${buildfile}.png" /><br /> <property name="dot.command.win" value="dot.exe -Tpng ${dot.file} -o ${png.file}" /><br /> <property name="dot.command.mac" value="dot -Tpng ${dot.file} -o ${png.file}" /><br /> <!-- Transform buildfile into DOT file --><br /> <xslt file="${buildfile}" tofile="${project.basedir}/${dot.file}" <br /> style="${phing2dot.xsl}" overwrite="true"> <br /> <param name="graph.label" expression="${buildfile}" /><br /> <param name="use.target.description" expression="true" /><br /> </xslt><br /> <!-- Generate image from DOT file --><br /> <exec command="${dot.command.win}" <br /> dir="${project.basedir}" os="WINNT" /><br /> <exec command="${dot.command.mac}" <br /> dir="${project.basedir}" os="Darwin" /><br /> <br /> <delete file="${project.basedir}/${dot.file}" /><br /> </target><br /> <br /></project></pre><h4 class="custom">Running the buildfile visualization target</h4>Now as mostly all necessary pieces are available it's time to check if the DOT command is available on the targeted platform by running a <em>dot(.exe) -V</em> on the console. If it isn't available it has to be installed, this might take several minutes depending on the given platform. Finally with everything in place the visualization process/target can be kicked off by calling Phing the<br />following way.<pre class="consoleOutput">triton:tmp stolt$ phing -f buildfile-visualizer.xml [-Dbuildfile=<targeted-buildfile.xml>]</pre>The last picture shows the visualization of the simple buildfile described in the <a href="http://phing.info/docs/guide/current/" target="_self">Phing Userguide</a> but it's also possible to get a meaningful <a href="http://www.flickr.com/photos/raphaelstolt/2974505537/" target="_self">visualization</a> of larger buildfiles like the one I currently use for <a href="http://raphaelstolt.blogspot.com/2007/08/setting-up-zend-framework applications.html" target="_self">setting up Zend Framework based projects</a>.<br /><a href="http://www.flickr.com/photos/raphaelstolt/2974636277/" title="Visualization of a simple buildfile by Raphael Stolt, on Flickr"><br /><img src="http://farm4.static.flickr.com/3030/2974636277_e31efcd536_o.png" width="350" height="133" alt="Visualization of a simple buildfile" border="0" /></a><br />Raphael Stolthttp://www.blogger.com/profile/07949831701855458792noreply@blogger.com1