Merge pull request #12830 from owncloud/intelligent-container

Intelligent container
remotes/origin/fix-10825
Thomas Müller 10 years ago
commit 40330bf492
  1. 44
      lib/private/appframework/app.php
  2. 177
      lib/private/appframework/dependencyinjection/dicontainer.php
  3. 85
      lib/private/appframework/utility/simplecontainer.php
  4. 38
      lib/private/route/router.php
  5. 16
      lib/public/appframework/app.php
  6. 28
      lib/public/appframework/queryexception.php
  7. 45
      tests/lib/appframework/AppTest.php
  8. 167
      tests/lib/appframework/utility/SimpleContainerTest.php

@ -24,8 +24,9 @@
namespace OC\AppFramework;
use OC\AppFramework\DependencyInjection\DIContainer;
use \OC_App;
use \OC\AppFramework\DependencyInjection\DIContainer;
use \OCP\AppFramework\QueryException;
/**
* Entry point for every request in your app. You can consider this as your
@ -36,6 +37,34 @@ use OC\AppFramework\DependencyInjection\DIContainer;
class App {
/**
* Turns an app id into a namespace by either reading the appinfo.xml's
* namespace tag or uppercasing the appid's first letter
* @param string $appId the app id
* @param string $topNamespace the namespace which should be prepended to
* the transformed app id, defaults to OCA\
* @return string the starting namespace for the app
*/
public static function buildAppNamespace($appId, $topNamespace='OCA\\') {
// first try to parse the app's appinfo/info.xml <namespace> tag
$filePath = OC_App::getAppPath($appId) . '/appinfo/info.xml';
$loadEntities = libxml_disable_entity_loader(false);
$xml = @simplexml_load_file($filePath);
libxml_disable_entity_loader($loadEntities);
if ($xml) {
$result = $xml->xpath('/info/namespace');
if ($result && count($result) > 0) {
// take first namespace result
return $topNamespace . trim((string) $result[0]);
}
}
// if the tag is not found, fall back to uppercasing the first letter
return $topNamespace . ucfirst($appId);
}
/**
* Shortcut for calling a controller method and printing the result
* @param string $controllerName the name of the controller under which it is
@ -48,7 +77,16 @@ class App {
if (!is_null($urlParams)) {
$container['urlParams'] = $urlParams;
}
$controller = $container[$controllerName];
$appName = $container['AppName'];
// first try $controllerName then go for \OCA\AppName\Controller\$controllerName
try {
$controller = $container->query($controllerName);
} catch(QueryException $e) {
$appNameSpace = self::buildAppNamespace($appName);
$controllerName = $appNameSpace . '\\Controller\\' . $controllerName;
$controller = $container->query($controllerName);
}
// initialize the dispatcher and run all the middleware before the controller
$dispatcher = $container['Dispatcher'];

@ -36,12 +36,13 @@ use OC\AppFramework\Utility\SimpleContainer;
use OC\AppFramework\Utility\TimeFactory;
use OC\AppFramework\Utility\ControllerMethodReflector;
use OCP\AppFramework\IApi;
use OCP\AppFramework\QueryException;
use OCP\AppFramework\IAppContainer;
use OCP\AppFramework\Middleware;
use OCP\IServerContainer;
class DIContainer extends SimpleContainer implements IAppContainer{
class DIContainer extends SimpleContainer implements IAppContainer {
/**
* @var array
@ -53,19 +54,181 @@ class DIContainer extends SimpleContainer implements IAppContainer{
* @param string $appName the name of the app
*/
public function __construct($appName, $urlParams = array()){
$this['AppName'] = $appName;
$this['urlParams'] = $urlParams;
$this->registerParameter('ServerContainer', \OC::$server);
/**
* Core services
*/
$this->registerService('OCP\\IAppConfig', function($c) {
return \OC::$server->getAppConfig();
});
$this->registerService('API', function($c){
return new API($c['AppName']);
$this->registerService('OCP\\IAppManager', function($c) {
return \OC::$server->getAppManager();
});
$this->registerService('OCP\\IAvatarManager', function($c) {
return \OC::$server->getAvatarManager();
});
$this->registerService('OCP\\Activity\\IManager', function($c) {
return \OC::$server->getActivityManager();
});
$this->registerService('OCP\\ICache', function($c) {
return \OC::$server->getCache();
});
$this->registerService('OCP\\ICacheFactory', function($c) {
return \OC::$server->getMemCacheFactory();
});
$this->registerService('OCP\\IConfig', function($c) {
return \OC::$server->getConfig();
});
$this->registerService('OCP\\Contacts\\IManager', function($c) {
return \OC::$server->getContactsManager();
});
$this->registerService('OCP\\IDateTimeZone', function($c) {
return \OC::$server->getDateTimeZone();
});
$this->registerService('OCP\\IDb', function($c) {
return \OC::$server->getDb();
});
$this->registerService('OCP\\IDBConnection', function($c) {
return \OC::$server->getDatabaseConnection();
});
$this->registerService('OCP\\Diagnostics\\IEventLogger', function($c) {
return \OC::$server->getEventLogger();
});
$this->registerService('OCP\\Diagnostics\\IQueryLogger', function($c) {
return \OC::$server->getQueryLogger();
});
$this->registerService('OCP\\Files\\Config\\IMountProviderCollection', function($c) {
return \OC::$server->getMountProviderCollection();
});
$this->registerService('OCP\\Files\\IRootFolder', function($c) {
return \OC::$server->getRootFolder();
});
$this->registerService('OCP\\IGroupManager', function($c) {
return \OC::$server->getGroupManager();
});
$this->registerService('OCP\\IL10N', function($c) {
return \OC::$server->getL10N($c->query('AppName'));
});
$this->registerService('OCP\\ILogger', function($c) {
return \OC::$server->getLogger();
});
$this->registerService('OCP\\BackgroundJob\\IJobList', function($c) {
return \OC::$server->getJobList();
});
$this->registerService('OCP\\AppFramework\\Utility\\IControllerMethodReflector', function($c) {
return $c->query('ControllerMethodReflector');
});
$this->registerService('OCP\\INavigationManager', function($c) {
return \OC::$server->getNavigationManager();
});
$this->registerService('OCP\\IPreview', function($c) {
return \OC::$server->getPreviewManager();
});
$this->registerService('OCP\\IRequest', function($c) {
return $c->query('Request');
});
$this->registerService('OCP\\ITagManager', function($c) {
return \OC::$server->getTagManager();
});
$this->registerService('OCP\\ITempManager', function($c) {
return \OC::$server->getTempManager();
});
$this->registerService('OCP\\AppFramework\\Utility\\ITimeFactory', function($c) {
return $c->query('TimeFactory');
});
$this->registerService('OCP\\Route\\IRouter', function($c) {
return \OC::$server->getRouter();
});
$this->registerService('OCP\\ISearch', function($c) {
return \OC::$server->getSearch();
});
$this->registerService('OCP\\ISearch', function($c) {
return \OC::$server->getSearch();
});
$this->registerService('OCP\\Security\\ICrypto', function($c) {
return \OC::$server->getCrypto();
});
$this->registerService('OCP\\Security\\IHasher', function($c) {
return \OC::$server->getHasher();
});
$this->registerService('OCP\\Security\\ISecureRandom', function($c) {
return \OC::$server->getSecureRandom();
});
$this->registerService('OCP\\IURLGenerator', function($c) {
return \OC::$server->getURLGenerator();
});
$this->registerService('OCP\\IUserManager', function($c) {
return \OC::$server->getUserManager();
});
$this->registerService('OCP\\IUserSession', function($c) {
return \OC::$server->getUserSession();
});
$this->registerService('ServerContainer', function ($c) {
$c->query('OCP\\ILogger')->warning(
'Accessing the server container is deprecated. Use type ' .
'annotations to inject core services instead!'
);
return \OC::$server;
});
// commonly used attributes
$this->registerService('UserId', function ($c) {
return $c->query('OCP\\IUserSession')->getSession()->get('user_id');
});
$this->registerService('WebRoot', function ($c) {
return $c->query('ServerContainer')->getWebRoot();
});
/**
* Http
* App Framework APIs
*/
$this->registerService('API', function($c){
$c->query('OCP\\ILogger')->warning(
'Accessing the API class is deprecated! Use the appropriate ' .
'services instead!'
);
return new API($c['AppName']);
});
$this->registerService('Request', function($c) {
/** @var $c SimpleContainer */
/** @var $server SimpleContainer */
@ -234,4 +397,6 @@ class DIContainer extends SimpleContainer implements IAppContainer{
}
\OCP\Util::writeLog($this->getAppName(), $message, $level);
}
}

@ -1,7 +1,29 @@
<?php
/**
* ownCloud - App Framework
*
* @author Bernhard Posselt
* @copyright 2014 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OC\AppFramework\Utility;
use \OCP\AppFramework\QueryException;
/**
* Class SimpleContainer
*
@ -9,12 +31,71 @@ namespace OC\AppFramework\Utility;
*/
class SimpleContainer extends \Pimple\Container implements \OCP\IContainer {
/**
* @param ReflectionClass $class the class to instantiate
* @return stdClass the created class
*/
private function buildClass(\ReflectionClass $class) {
$constructor = $class->getConstructor();
if ($constructor === null) {
return $class->newInstance();
} else {
$parameters = [];
foreach ($constructor->getParameters() as $parameter) {
$parameterClass = $parameter->getClass();
// try to find out if it is a class or a simple parameter
if ($parameterClass === null) {
$resolveName = $parameter->getName();
} else {
$resolveName = $parameterClass->name;
}
$parameters[] = $this->query($resolveName);
}
return $class->newInstanceArgs($parameters);
}
}
/**
* If a parameter is not registered in the container try to instantiate it
* by using reflection to find out how to build the class
* @param string $name the class name to resolve
* @throws QueryException if the class could not be found or instantiated
*/
private function resolve($name) {
$baseMsg = 'Could not resolve ' . $name . '!';
try {
$class = new \ReflectionClass($name);
if ($class->isInstantiable()) {
return $this->buildClass($class);
} else {
throw new QueryException($baseMsg .
' Class can not be instantiated');
}
} catch(\ReflectionException $e) {
throw new QueryException($baseMsg . ' ' . $e->getMessage());
}
}
/**
* @param string $name name of the service to query for
* @return mixed registered service for the given $name
* @throws QueryExcpetion if the query could not be resolved
*/
public function query($name) {
return $this->offsetGet($name);
if ($this->offsetExists($name)) {
return $this->offsetGet($name);
} else {
$object = $this->resolve($name);
$this->registerService($name, function () use ($object) {
return $object;
});
return $object;
}
}
/**
@ -44,4 +125,6 @@ class SimpleContainer extends \Pimple\Container implements \OCP\IContainer {
$this[$name] = parent::factory($closure);
}
}
}

@ -9,6 +9,7 @@
namespace OC\Route;
use OCP\Route\IRouter;
use OCP\AppFramework\App;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Generator\UrlGenerator;
use Symfony\Component\Routing\RequestContext;
@ -129,7 +130,7 @@ class Router implements IRouter {
if (!isset($this->loadedApps[$app])) {
$this->loadedApps[$app] = true;
$this->useCollection($app);
$this->requireRouteFile($file);
$this->requireRouteFile($file, $app);
$collection = $this->getCollection($app);
$collection->addPrefix('/apps/' . $app);
$this->root->addCollection($collection);
@ -283,10 +284,39 @@ class Router implements IRouter {
/**
* To isolate the variable scope used inside the $file it is required in it's own method
* @param string $file
* @param string $file the route file location to include
* @param string $appName
*/
private function requireRouteFile($file) {
require_once $file;
private function requireRouteFile($file, $appName) {
$this->setupRoutes(include_once $file, $appName);
}
/**
* If a routes.php file returns an array, try to set up the application and
* register the routes for the app. The application class will be chosen by
* camelcasing the appname, e.g.: my_app will be turned into
* \OCA\MyApp\AppInfo\Application. If that class does not exist, a default
* App will be intialized. This makes it optional to ship an
* appinfo/application.php by using the built in query resolver
* @param array $routes the application routes
* @param string $appName the name of the app.
*/
private function setupRoutes($routes, $appName) {
if (is_array($routes)) {
$appNameSpace = App::buildAppNamespace($appName);
$applicationClassName = $appNameSpace . '\\AppInfo\\Application';
if (class_exists($applicationClassName)) {
$application = new $applicationClassName();
} else {
$application = new App($appName);
}
$application->registerRoutes($this, $routes);
}
}
}

@ -37,6 +37,22 @@ use OC\AppFramework\routing\RouteConfig;
* to be registered using IContainer::registerService
*/
class App {
/**
* Turns an app id into a namespace by convetion. The id is split at the
* underscores, all parts are camelcased and reassembled. e.g.:
* some_app_id -> OCA\SomeAppId
* @param string $appId the app id
* @param string $topNamespace the namespace which should be prepended to
* the transformed app id, defaults to OCA\
* @return string the starting namespace for the app
*/
public static function buildAppNamespace($appId, $topNamespace='OCA\\') {
return \OC\AppFramework\App::buildAppNamespace($appId, $topNamespace);
}
/**
* @param array $urlParams an array with variables extracted from the routes
*/

@ -0,0 +1,28 @@
<?php
/**
* ownCloud - App Framework
*
* @author Bernhard Posselt
* @copyright 2014 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\AppFramework;
use Exception;
class QueryException extends Exception {}

@ -24,6 +24,17 @@
namespace OC\AppFramework;
function rrmdir($directory) {
$files = array_diff(scandir($directory), array('.','..'));
foreach ($files as $file) {
if (is_dir($directory . '/' . $file)) {
rrmdir($directory . '/' . $file);
} else {
unlink($directory . '/' . $file);
}
}
return rmdir($directory);
}
class AppTest extends \Test\TestCase {
@ -36,6 +47,7 @@ class AppTest extends \Test\TestCase {
private $output;
private $controllerName;
private $controllerMethod;
private $appPath;
protected function setUp() {
parent::setUp();
@ -59,6 +71,17 @@ class AppTest extends \Test\TestCase {
$this->container[$this->controllerName] = $this->controller;
$this->container['Dispatcher'] = $this->dispatcher;
$this->container['urlParams'] = array();
$this->appPath = __DIR__ . '/../../../apps/namespacetestapp/appinfo';
$infoXmlPath = $this->appPath . '/info.xml';
mkdir($this->appPath, 0777, true);
$xml = '<?xml version="1.0" encoding="UTF-8"?>' .
'<info>' .
'<id>namespacetestapp</id>' .
'<namespace>NameSpaceTestApp</namespace>' .
'</info>';
file_put_contents($infoXmlPath, $xml);
}
@ -77,6 +100,28 @@ class AppTest extends \Test\TestCase {
}
public function testBuildAppNamespace() {
$ns = App::buildAppNamespace('someapp');
$this->assertEquals('OCA\Someapp', $ns);
}
public function testBuildAppNamespaceCore() {
$ns = App::buildAppNamespace('someapp', 'OC\\');
$this->assertEquals('OC\Someapp', $ns);
}
public function testBuildAppNamespaceInfoXml() {
$ns = App::buildAppNamespace('namespacetestapp', 'OCA\\');
$this->assertEquals('OCA\NameSpaceTestApp', $ns);
}
protected function tearDown() {
rrmdir($this->appPath);
}
/*
FIXME: this complains about shit headers which are already sent because
of the content length. Would be cool if someone could fix this

@ -0,0 +1,167 @@
<?php
/**
* ownCloud - App Framework
*
* @author Bernhard Posselt
* @copyright 2014 Bernhard Posselt <dev@bernhard-posselt.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU AFFERO GENERAL PUBLIC LICENSE for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace Test\AppFramework\Utility;
use OC\AppFramework\Utility\SimpleContainer;
interface TestInterface {}
class ClassEmptyConstructor implements IInterfaceConstructor {}
class ClassSimpleConstructor implements IInterfaceConstructor {
public $test;
public function __construct($test) {
$this->test = $test;
}
}
class ClassComplexConstructor {
public $class;
public $test;
public function __construct(ClassSimpleConstructor $class, $test) {
$this->class = $class;
$this->test = $test;
}
}
interface IInterfaceConstructor {}
class ClassInterfaceConstructor {
public $class;
public $test;
public function __construct(IInterfaceConstructor $class, $test) {
$this->class = $class;
$this->test = $test;
}
}
class SimpleContainerTest extends \Test\TestCase {
private $container;
public function setUp() {
$this->container = new SimpleContainer();
}
public function testRegister() {
$this->container->registerParameter('test', 'abc');
$this->assertEquals('abc', $this->container->query('test'));
}
/**
* @expectedException \OCP\AppFramework\QueryException
*/
public function testNothingRegistered() {
$this->container->query('something really hard');
}
/**
* @expectedException \OCP\AppFramework\QueryException
*/
public function testNotAClass() {
$this->container->query('Test\AppFramework\Utility\TestInterface');
}
public function testNoConstructorClass() {
$object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor');
$this->assertTrue($object instanceof ClassEmptyConstructor);
}
public function testInstancesOnlyOnce() {
$object = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor');
$object2 = $this->container->query('Test\AppFramework\Utility\ClassEmptyConstructor');
$this->assertSame($object, $object2);
}
public function testConstructorSimple() {
$this->container->registerParameter('test', 'abc');
$object = $this->container->query(
'Test\AppFramework\Utility\ClassSimpleConstructor'
);
$this->assertTrue($object instanceof ClassSimpleConstructor);
$this->assertEquals('abc', $object->test);
}
public function testConstructorComplex() {
$this->container->registerParameter('test', 'abc');
$object = $this->container->query(
'Test\AppFramework\Utility\ClassComplexConstructor'
);
$this->assertTrue($object instanceof ClassComplexConstructor);
$this->assertEquals('abc', $object->class->test);
$this->assertEquals('abc', $object->test);
}
public function testConstructorComplexInterface() {
$this->container->registerParameter('test', 'abc');
$this->container->registerService(
'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) {
return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor');
});
$object = $this->container->query(
'Test\AppFramework\Utility\ClassInterfaceConstructor'
);
$this->assertTrue($object instanceof ClassInterfaceConstructor);
$this->assertEquals('abc', $object->class->test);
$this->assertEquals('abc', $object->test);
}
public function tesOverrideService() {
$this->container->registerParameter('test', 'abc');
$this->container->registerService(
'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) {
return $c->query('Test\AppFramework\Utility\ClassSimpleConstructor');
});
$this->container->registerService(
'Test\AppFramework\Utility\IInterfaceConstructor', function ($c) {
return $c->query('Test\AppFramework\Utility\ClassEmptyConstructor');
});
$object = $this->container->query(
'Test\AppFramework\Utility\ClassInterfaceConstructor'
);
$this->assertTrue($object instanceof ClassEmptyConstructor);
$this->assertEquals('abc', $object->test);
}
/**
* @expectedException \OCP\AppFramework\QueryException
*/
public function testConstructorComplexNoTestParameterFound() {
$object = $this->container->query(
'Test\AppFramework\Utility\ClassComplexConstructor'
);
}
}
Loading…
Cancel
Save