diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6022e5e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +docs export-ignore +tests export-ignore diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..7579f74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +vendor +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f95aa40 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3.3 + - 5.4 + +before_script: + - composer self-update + - composer install --no-interaction --prefer-dist --quiet --dev + +script: php tests/lint.php src/Kdyby/ tests/KdybyTests/ && VERBOSE=true ./tests/run-tests.sh -s tests/KdybyTests/ diff --git a/README.md b/README.md new file mode 100755 index 0000000..80003d0 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +Kdyby/Autowired [![Build Status](https://secure.travis-ci.org/Kdyby/Autowired.png?branch=master)](http://travis-ci.org/Kdyby/Autowired) +=========================== + + +Requirements +------------ + +Kdyby/Autowired requires PHP 5.3.2 or higher. + +- [Nette Framework 2.0.x](https://github.com/nette/nette) + + +Installation +------------ + +The best way to install Kdyby/Autowired is using [Composer](http://getcomposer.org/): + +```sh +$ composer require kdyby/autowired:@dev +``` + + +----- + +Homepage [http://www.kdyby.org](http://www.kdyby.org) and repository [http://github.com/kdyby/autowired](http://github.com/kdyby/autowired). diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..06af102 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "kdyby/autowired", + "type": "library", + "description": "DI Extension for Nette Framework", + "keywords": ["nette", "di", "autowire", "kdyby"], + "homepage": "http://kdyby.org", + "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], + "authors": [ + { + "name": "Filip Procházka", + "homepage": "http://filip-prochazka.com" + } + ], + "support": { + "email": "filip@prochazka.su", + "issues": "https://github.com/kdyby/autowired/issues" + }, + "require": { + "nette/nette": "@dev" + }, + "require-dev": { + "nette/tester": "@dev" + }, + "autoload": { + "psr-0": { + "Kdyby\\Autowired": "src/" + }, + "classmap": [ + "src/Kdyby/Autowired/exceptions.php" + ] + } +} diff --git a/license.md b/license.md new file mode 100644 index 0000000..835b659 --- /dev/null +++ b/license.md @@ -0,0 +1,57 @@ +Licenses +======== + +Good news! You may use Kdyby Framework under the terms of either +the New BSD License or the GNU General Public License (GPL) version 2 or 3. + +The BSD License is recommended for most projects. It is easy to understand and it +places almost no restrictions on what you can do with the framework. If the GPL +fits better to your project, you can use the framework under this license. + +You don't have to notify anyone which license you are using. You can freely +use Kdyby Framework in commercial projects as long as the copyright header +remains intact. + + + +New BSD License +--------------- + +Copyright (c) 2008 Filip Procházka (http://filip-prochazka.com) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name of "Kdyby Framework" nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as is" and +any express or implied warranties, including, but not limited to, the implied +warranties of merchantability and fitness for a particular purpose are +disclaimed. In no event shall the copyright owner or contributors be liable for +any direct, indirect, incidental, special, exemplary, or consequential damages +(including, but not limited to, procurement of substitute goods or services; +loss of use, data, or profits; or business interruption) however caused and on +any theory of liability, whether in contract, strict liability, or tort +(including negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + + + +GNU General Public License +-------------------------- + +GPL licenses are very very long, so instead of including them here we offer +you URLs with full text: + +- [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) +- [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) diff --git a/src/Kdyby/Autowired/AutowireProperties.php b/src/Kdyby/Autowired/AutowireProperties.php new file mode 100644 index 0000000..e69eb9e --- /dev/null +++ b/src/Kdyby/Autowired/AutowireProperties.php @@ -0,0 +1,197 @@ + + */ +trait AutowireProperties +{ + + /** + * @var array + */ + private $autowire = array(); + + /** + * @var Nette\DI\Container + */ + private $autowirePropertiesLocator; + + + + /** + * @param \Nette\DI\Container $dic + * @throws MemberAccessException + * @throws MissingServiceException + * @throws InvalidStateException + * @throws UnexpectedValueException + */ + public function injectProperties(Nette\DI\Container $dic) + { + if (!$this instanceof Nette\Application\UI\PresenterComponent) { + throw new MemberAccessException('Trait ' . __TRAIT__ . ' can be used only in descendants of PresenterComponent.'); + } + + $this->autowirePropertiesLocator = $dic; + $cache = new Nette\Caching\Cache($dic->getByType('Nette\Caching\IStorage'), 'Kdyby.Autowired.PresenterComponent'); + if (($this->autowire = $cache->load($presenterClass = get_class($this))) === NULL) { + $this->autowire = array(); + + $rc = ClassType::from($this); + $ignore = class_parents('Nette\Application\UI\Presenter') + array('ui' => 'Nette\Application\UI\Presenter'); + foreach ($rc->getProperties(Property::IS_PUBLIC | Property::IS_PROTECTED) as $prop) { + /** @var Property $prop */ + if (in_array($prop->getDeclaringClass()->getName(), $ignore) || !$prop->hasAnnotation('autowire')) { + continue; + } + + $this->resolveProperty($prop); + } + + $files = array_map(function ($class) { + return ClassType::from($class)->getFileName(); + }, array_diff(array_values(class_parents($presenterClass) + array('me' => $presenterClass)), $ignore)); + + $cache->save($presenterClass, $this->autowire, array( + $cache::FILES => $files, + )); + + } else { + foreach ($this->autowire as $propName => $tmp) { + unset($this->{$propName}); + } + } + } + + + + /** + * @param Property $prop + * @throws MissingServiceException + * @throws UnexpectedValueException + */ + private function resolveProperty(Property $prop) + { + $type = $this->resolveAnnotationClass($prop, $prop->getAnnotation('var'), 'var'); + $metadata = array( + 'value' => NULL, + 'type' => $type, + ); + + if (($args = (array) $prop->getAnnotation('autowire')) && !empty($args['factory'])) { + $factoryType = $this->resolveAnnotationClass($prop, $args['factory'], 'autowire'); + + if (empty($this->autowirePropertiesLocator->classes[strtolower($factoryType)])) { + throw new MissingServiceException("Factory of type \"$factoryType\" not found for $prop in annotation @autowire."); + } + + $factoryMethod = Method::from($factoryType, 'create'); + $createsType = $this->resolveAnnotationClass($factoryMethod, $factoryMethod->getAnnotation('return'), 'return'); + if ($createsType !== $type) { + throw new UnexpectedValueException("The property $prop requires $type, but factory of type $factoryType, that creates $createsType was provided."); + } + + unset($args['factory']); + $metadata['arguments'] = array_values($args); + $metadata['factory'] = $this->autowirePropertiesLocator->classes[strtolower($factoryType)]; + + } else { + if (empty($this->autowirePropertiesLocator->classes[strtolower($type)])) { + throw new MissingServiceException("Service of type \"$type\" not found for $prop in annotation @var."); + } + } + + // unset property to pass control to __set() and __get() + unset($this->{$prop->getName()}); + $this->autowire[$prop->getName()] = $metadata; + } + + + + private function resolveAnnotationClass(\Reflector $prop, $annotationValue, $annotationName) + { + /** @var Property|Method $prop */ + + if (!$type = ltrim($annotationValue, '\\')) { + throw new InvalidStateException("Missing annotation @{$annotationName} with typehint on {$prop}."); + } + + if (!class_exists($type) && !interface_exists($type)) { + if (substr(func_get_arg(1), 0, 1) === '\\') { + throw new MissingClassException("Class \"$type\" was not found, please check the typehint on {$prop} in annotation @{$annotationName}"); + } + + if (!class_exists($type = $prop->getDeclaringClass()->getNamespaceName() . '\\' . $type) && !interface_exists($type)) { + throw new MissingClassException("Neither class \"" . func_get_arg(1) . "\" or \"{$type}\" was found, please check the typehint on {$prop} in annotation @{$annotationName}"); + } + } + + return ClassType::from($type)->getName(); + } + + + + /** + * @param string $name + * @param mixed $value + * @throws MemberAccessException + * @return mixed + */ + public function __set($name, $value) + { + if (!isset($this->autowire[$name])) { + return parent::__set($name, $value); + + } elseif ($this->autowire[$name]['value']) { + throw new MemberAccessException("Property \$$name has already been set."); + + } elseif (!$value instanceof $this->autowire[$name]['type']) { + throw new MemberAccessException("Property \$$name must be an instance of " . $this->autowire[$name]['type'] . "."); + } + + return $this->autowire[$name]['value'] = $value; + } + + + + /** + * @param $name + * @throws MemberAccessException + * @return mixed + */ + public function &__get($name) + { + if (!isset($this->autowire[$name])) { + return parent::__get($name); + } + + if (empty($this->autowire[$name]['value'])) { + if (!empty($this->autowire[$name]['factory'])) { + $factory = callback($this->autowirePropertiesLocator->getService($this->autowire[$name]['factory']), 'create'); + $this->autowire[$name]['value'] = $factory->invokeArgs($this->autowire[$name]['arguments']); + + } else { + $this->autowire[$name]['value'] = $this->autowirePropertiesLocator->getByType($this->autowire[$name]['type']); + } + } + + return $this->autowire[$name]['value']; + } + +} diff --git a/src/Kdyby/Autowired/exceptions.php b/src/Kdyby/Autowired/exceptions.php new file mode 100644 index 0000000..4b1008a --- /dev/null +++ b/src/Kdyby/Autowired/exceptions.php @@ -0,0 +1,62 @@ + + * @package Kdyby\Autowired + */ + +namespace KdybyTests\Autowired; + +use Kdyby; +use Nette; +use Nette\DI; +use Nette\PhpGenerator\PhpLiteral; +use Tester; +use Tester\Assert; + +require_once __DIR__ . '/../bootstrap.php'; + + + +/** + * @author Filip Procházka + */ +class AutowirePropertiesTest extends Tester\TestCase +{ + + /** + * @var Nette\DI\Container + */ + private $container; + + + + protected function setUp() + { + $builder = new DI\ContainerBuilder; + $builder->addDefinition('sampleFactory') + ->setFactory('KdybyTests\Autowired\SampleService', array(new PhpLiteral('$name'), new PhpLiteral('$secondName'))) + ->setImplement('KdybyTests\Autowired\ISampleServiceFactory') + ->setParameters(array('name', 'secondName' => NULL)) + ->setShared(TRUE)->setAutowired(TRUE); + + $builder->addDefinition('sample') + ->setClass('KdybyTests\Autowired\SampleService', array('shared')); + + $builder->addDefinition('cacheStorage') + ->setClass('Nette\Caching\Storages\MemoryStorage'); + + // run-time + $code = implode('', $builder->generateClasses()); + file_put_contents(TEMP_DIR . '/code.php', "container = new \Container; + } + + + + public function testFunctional() + { + $presenter = new DummyPresenter(); + Assert::null($presenter->service); + Assert::null($presenter->factoryResult); + Assert::null($presenter->secondFactoryResult); + + $this->container->callMethod(array($presenter, 'injectProperties')); + + Assert::true($presenter->service instanceof SampleService); + Assert::same(array('shared'), $presenter->service->args); + + Assert::true($presenter->factoryResult instanceof SampleService); + Assert::same(array('string argument', NULL), $presenter->factoryResult->args); + + Assert::true($presenter->secondFactoryResult instanceof SampleService); + Assert::same(array('string argument', 'and another'), $presenter->secondFactoryResult->args); + } + +} + + +class DummyPresenter extends Nette\Application\UI\Presenter +{ + + use Kdyby\Autowired\AutowireProperties; + + /** + * @var SampleService + * @autowire + */ + public $service; + + /** + * @var SampleService + * @autowire("string argument", factory=\KdybyTests\Autowired\ISampleServiceFactory) + */ + public $factoryResult; + + /** + * @var SampleService + * @autowire("string argument", "and another", factory=\KdybyTests\Autowired\ISampleServiceFactory) + */ + public $secondFactoryResult; + +} + +class SampleService +{ + public $args; + + public function __construct($name, $secondName = NULL) + { + $this->args = func_get_args(); + } +} + +interface ISampleServiceFactory +{ + /** @return SampleService */ + function create($name, $secondName = NULL); +} + + +\run(new AutowirePropertiesTest()); diff --git a/tests/KdybyTests/bootstrap.php b/tests/KdybyTests/bootstrap.php new file mode 100755 index 0000000..357bfc4 --- /dev/null +++ b/tests/KdybyTests/bootstrap.php @@ -0,0 +1,42 @@ +run(isset($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : NULL); +} diff --git a/tests/conventions.txt b/tests/conventions.txt new file mode 100644 index 0000000..bf471e6 --- /dev/null +++ b/tests/conventions.txt @@ -0,0 +1,26 @@ +Test case file name +=================== + +Nette\....phpt + +Nette\Debug.phpt - tests for a class's basic behaviour +Nette\Debug.fireLog().phpt - tests for a method's basic behaviour +Nette\Debug.fireLog().inc - common code for more test cases +Nette\Debug.fireLog().expect - expected raw output +Nette\Debug.fireLog().area.phpt - tests for a specified area of class/method + +- areas: basic, error, bug#123 +- numbers have three digits + + +Test case phpDoc +================ + +/** + * Test: some test name + * + * @author John Doe + * @phpVersion < 5.3 default operator is >= + * @skip some reason why test is skipped + * @phpIni short_open_tag=on + */ diff --git a/tests/lint.php b/tests/lint.php new file mode 100644 index 0000000..7998d7e --- /dev/null +++ b/tests/lint.php @@ -0,0 +1,103 @@ +#!/usr/bin/php + false, 'files' => array()); + foreach (array_keys(getopt('qh', array('quiet', 'help'))) as $arg) { + switch ($arg) { + case 'q': + case 'quiet': + $options['quiet'] = true; + break; + + case 'h': + case 'help': + default: + echo << 1) { + foreach ($_SERVER['argv'] as $i => $arg) { + if (substr($arg, 0, 1) === '-' || $i === 0) continue; + $options['files'][] = $arg; + } + } + + if (empty($options['files'])) $options['files'][] = $_SERVER['PWD']; + + foreach ($options['files'] as $i => $file) { + if (($options['files'][$i] = realpath($file)) !== false) continue; + echo "$file is not a file or directory.\n"; + exit(1); + } + + return $options; +}; + +$echo = function () use (&$context) { + if ($context['quiet']) return; + foreach (func_get_args() as $arg) echo $arg; +}; + +$lintFile = function ($path) use (&$echo, &$context) { + if (substr($path, -4) != '.php') return; + + if ($context['filesCount'] % 63 == 0) { + $echo("\n"); + } + + exec("php -l " . escapeshellarg($path) . " 2>&1 1> /dev/null", $output, $code); + if ($code) { + $context['errors'][] = implode($output); + $echo('E'); + } else { + $echo('.'); + } + + $context['filesCount']++; +}; + +$check = function ($path) use (&$check, &$lintFile, &$context) { + if (!is_dir($path)) return $lintFile($path); + foreach (scandir($path) as $item) { + if ($item == '.' || $item == '..') continue; + $check(rtrim($path, '/') . '/' . $item); + } +}; + + +$context = $parseOptions(); +$context['filesCount'] = 0; +$context['errors'] = array(); +foreach ($context['files'] as $file) $check($file); +if ($context['errors']) { + $echo("\n\n", implode($context['errors'])); +} + +$echo( + "\n\n", ($context['errors'] ? 'FAILED' : 'OK'), + ' (', $context['filesCount'], " files checked, ", count($context['errors']), " errors)\n" +); +exit($context['errors'] ? 1 : 0); diff --git a/tests/php.ini-unix b/tests/php.ini-unix new file mode 100644 index 0000000..e69de29 diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..ff71cc5 --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +# Path to this script's directory +dir=$(cd `dirname $0` && pwd) + +# Path to test runner script +runnerScript="$dir/../vendor/nette/tester/Tester/tester.php" +if [ ! -f "$runnerScript" ]; then + echo "Nette Tester is missing. You can install it using Composer:" >&2 + echo "php composer.phar update --dev." >&2 + exit 2 +fi + +# Path to php.ini if passed as argument option +phpIni= +while getopts ":c:" opt; do + case $opt in + c) phpIni="$OPTARG" + ;; + + :) echo "Missing argument for -$OPTARG option" >&2 + exit 2 + ;; + esac +done + +# Runs tests with script's arguments, add default php.ini if not specified +# Doubled -c option intentionally +if [ -n "$phpIni" ]; then + php -c "$phpIni" "$runnerScript" -j 20 "$@" +else + php -c "$dir/php.ini-unix" "$runnerScript" -j 20 -c "$dir/php.ini-unix" "$@" +fi +error=$? + +# Print *.actual content if tests failed +if [ "${VERBOSE-false}" != "false" -a $error -ne 0 ]; then + for i in $(find . -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + exit $error +fi diff --git a/tests/tmp/.gitignore b/tests/tmp/.gitignore new file mode 100755 index 0000000..816b594 --- /dev/null +++ b/tests/tmp/.gitignore @@ -0,0 +1,2 @@ +* +!.* \ No newline at end of file