// save components to be enabled
if (array_key_exists('enableComponents', $params)) {
- CRM_Core_BAO_Setting::setItem($params['enableComponents'],
- CRM_Core_BAO_Setting::SYSTEM_PREFERENCES_NAME, 'enable_components');
-
- // unset params by emptying the values, so while retrieving we can detect and load from settings table
- // instead of config-backend for backward compatibility. We could use unset() in later releases.
- $params['enableComponents'] = $params['enableComponentIDs'] = array();
+ civicrm_api3('setting', 'create', array(
+ 'enable_components' => $params['enableComponents'],
+ ));
+ unset($params['enableComponents']);
}
// save checksum timeout
public function postProcess() {
$params = $this->controller->exportValues($this->_name);
- CRM_Case_Info::onToggleComponents($this->_defaults['enableComponents'], $params['enableComponents'], NULL);
parent::commonProcess($params);
// reset navigation when components are enabled / disabled
if (isset($metadata['on_change'])) {
foreach ($metadata['on_change'] as $callback) {
- call_user_func($callback, unserialize($dao->value), $value, $metadata);
+ call_user_func(
+ Civi\Core\Resolver::singleton()->get($callback),
+ unserialize($dao->value),
+ $value,
+ $metadata
+ );
}
}
return TRUE;
}
else {
- list($class, $fn) = explode('::', $fieldSpec['validate_callback']);
- if (!$class::$fn($value, $fieldSpec)) {
+ $cb = Civi\Core\Resolver::singleton()->get($fieldSpec['validate_callback']);
+ if (!call_user_func_array($cb, array(&$value, $fieldSpec))) {
throw new api_Exception("validation failed for {$fieldSpec['name']} = $value based on callback {$fieldSpec['validate_callback']}");
}
}
}
$result = NULL;
- if (is_array($item['page_callback'])) {
- if ($item['page_callback']{0} !== '\\') {
- // Legacy class-loading for PHP 5.2 namespaces; not sure it's needed, but counter-productive for PHP 5.3 namespaces
- require_once str_replace('_', DIRECTORY_SEPARATOR, $item['page_callback'][0]) . '.php';
- }
- $result = call_user_func($item['page_callback']);
+ // WISHLIST: Refactor this. Instead of pattern-matching on page_callback, lookup
+ // page_callback via Civi\Core\Resolver and check the implemented interfaces. This
+ // would require rethinking the default constructor.
+ if (is_array($item['page_callback']) || strpos($item['page_callback'], ':')) {
+ $result = call_user_func(Civi\Core\Resolver::singleton()->get($item['page_callback']));
}
elseif (strstr($item['page_callback'], '_Form')) {
$wrapper = new CRM_Utils_Wrapper();
}
else {
$newArgs = explode('/', $_GET[$config->userFrameworkURLVar]);
- if ($item['page_callback']{0} !== '\\') {
- // Legacy class-loading for PHP 5.2 namespaces; not sure it's needed, but counter-productive for PHP 5.3 namespaces
- require_once str_replace('_', DIRECTORY_SEPARATOR, $item['page_callback']) . '.php';
- }
$mode = 'null';
if (isset($pageArgs['mode'])) {
$mode = $pageArgs['mode'];
if (strpos($key, '_callback') &&
strpos($value, '::')
) {
+ // FIXME Remove the rewrite at this level. Instead, change downstream call_user_func*($value)
+ // to call_user_func*(Civi\Core\Resolver::singleton()->get($value)).
$value = explode('::', $value);
}
elseif ($key == 'access_arguments') {
+ // FIXME Move the permission parser to its own class (or *maybe* CRM_Core_Permission).
if (strpos($value, ',') ||
strpos($value, ';')
) {
// if callback is specified..
if (!empty($pseudoconstant['callback'])) {
- list($className, $fnName) = explode('::', $pseudoconstant['callback']);
- if (method_exists($className, $fnName)) {
- return call_user_func(array($className, $fnName));
- }
+ return call_user_func(Civi\Core\Resolver::singleton()->get($pseudoconstant['callback']));
}
// Merge params with schema defaults
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Doctrine\ORM\Tools\Setup;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
))
->setFactoryService(self::SELF)->setFactoryMethod('createApiKernel');
+ // Expose legacy singletons as services in the container.
+ $singletons = array(
+ 'resources' => 'CRM_Core_Resources',
+ 'httpClient' => 'CRM_Utils_HttpClient',
+ // Maybe? 'config' => 'CRM_Core_Config',
+ // Maybe? 'smarty' => 'CRM_Core_Smarty',
+ );
+ foreach ($singletons as $name => $class) {
+ $container->setDefinition($name, new Definition(
+ $class
+ ))
+ ->setFactoryClass($class)->setFactoryMethod('singleton');
+ }
+
return $container;
}
--- /dev/null
+<?php
+namespace Civi\Core;
+
+/**
+ * The resolver takes a string expression and returns an object or callable.
+ *
+ * The following patterns will resolve to objects:
+ * - 'obj://objectName' - An object from Civi\Core\Container
+ * - 'ClassName' - An instance of ClassName (with default constructor).
+ * If you need more control over construction, then register with the
+ * container.
+ *
+ * The following patterns will resolve to callables:
+ * - 'function_name' - A function(callable).
+ * - 'ClassName::methodName" - A static method of a class.
+ * - 'call://objectName/method' - A method on an object from Civi\Core\Container.
+ * - 'api3://EntityName/action' - A method call on an API.
+ * (Performance note: Requires full setup/teardown of API subsystem.)
+ * - 'api3://EntityName/action?first=@1&second=@2' - Call an API method, mapping the
+ * first & second args to named parameters.
+ * (Performance note: Requires parsing/interpolating arguments).
+ * - '0' or '1' - A dummy which returns the constant '0' or '1'.
+ *
+ * Note: To differentiate classes and functions, there is a hard requirement that
+ * class names begin with an uppercase letter.
+ *
+ * Note: If you are working in a context which requires a callable, it is legitimate to use
+ * an object notation ("obj://objectName" or "ClassName") if the object supports __invoke().
+ *
+ * @package Civi\Core
+ */
+class Resolver {
+
+ protected static $_singleton;
+
+ /**
+ * @return Resolver
+ */
+ public static function singleton() {
+ if (self::$_singleton === NULL) {
+ self::$_singleton = new Resolver();
+ }
+ return self::$_singleton;
+ }
+
+ /**
+ * Convert a callback expression to a valid PHP callback.
+ *
+ * @param string|array $id
+ * A callback expression; any of the following.
+ * @return array
+ * A PHP callback. Do not serialize (b/c it may include an object).
+ */
+ public function get($id) {
+ if (!is_string($id)) {
+ // An array or object does not need to be further resolved.
+ return $id;
+ }
+
+ if (strpos($id, '::') !== FALSE) {
+ // Callback: Static method.
+ return explode('::', $id);
+ }
+ elseif (strpos($id, '://') !== FALSE) {
+ $url = parse_url($id);
+ switch ($url['scheme']) {
+ case 'obj':
+ // Object: Lookup in container.
+ return Container::singleton()->get($url['host']);
+
+ case 'call':
+ // Callback: Object/method in container.
+ $obj = Container::singleton()->get($url['host']);
+ return array($obj, ltrim($url['path'], '/'));
+
+ case 'api3':
+ // Callback: API.
+ return new ResolverApi($url);
+
+ default:
+ throw new \RuntimeException("Unsupported callback scheme: " . $url['scheme']);
+ }
+ }
+ elseif (in_array($id, array('0', '1'))) {
+ // Callback: Constant value.
+ return new ResolverConstantCallback((int) $id);
+ }
+ elseif ($id{0} >= 'A' && $id{0} <= 'Z') {
+ // Object: New/default instance.
+ return new $id();
+ }
+ else {
+ // Callback: Function.
+ return $id;
+ }
+ }
+
+}
+
+/**
+ * Private helper which produces a dummy callback.
+ *
+ * @package Civi\Core
+ */
+class ResolverConstantCallback {
+ /**
+ * @var mixed
+ */
+ private $value;
+
+ /**
+ * @param mixed $value
+ * The value to be returned by the dummy callback.
+ */
+ public function __construct($value) {
+ $this->value = $value;
+ }
+
+ /**
+ * @return mixed
+ */
+ public function __invoke() {
+ return $this->value;
+ }
+
+}
+
+/**
+ * Private helper which treats an API as a callable function.
+ *
+ * @package Civi\Core
+ */
+class ResolverApi {
+ /**
+ * @var array
+ * - string scheme
+ * - string host
+ * - string path
+ * - string query (optional)
+ */
+ private $url;
+
+ /**
+ * @param array $url
+ * Parsed URL (e.g. "api3://EntityName/action?foo=bar").
+ * @see parse_url
+ */
+ public function __construct($url) {
+ $this->url = $url;
+ }
+
+ /**
+ * Fire an API call.
+ */
+ public function __invoke() {
+ $apiParams = array();
+ if (isset($this->url['query'])) {
+ parse_str($this->url['query'], $apiParams);
+ }
+
+ if (count($apiParams)) {
+ $args = func_get_args();
+ if (count($args)) {
+ $this->interpolate($apiParams, $this->createPlaceholders('@', $args));
+ }
+ }
+
+ $result = civicrm_api3($this->url['host'], ltrim($this->url['path'], '/'), $apiParams);
+ return isset($result['values']) ? $result['values'] : NULL;
+ }
+
+ /**
+ * @param array $args
+ * Positional arguments.
+ * @return array
+ * Named placeholders based on the positional arguments
+ * (e.g. "@1" => "firstValue").
+ */
+ protected function createPlaceholders($prefix, $args) {
+ $result = array();
+ foreach ($args as $offset => $arg) {
+ $result[$prefix . (1 + $offset)] = $arg;
+ }
+ return $result;
+ }
+
+ /**
+ * Recursively interpolate values.
+ *
+ * @code
+ * $params = array('foo' => '@1');
+ * $this->interpolate($params, array('@1'=> $object))
+ * assert $data['foo'] == $object;
+ * @endcode
+ *
+ * @param array $array
+ * Array which may or many not contain a mix of tokens.
+ * @param array $replacements
+ * A list of tokens to substitute.
+ */
+ protected function interpolate(&$array, $replacements) {
+ foreach (array_keys($array) as $key) {
+ if (is_array($array[$key])) {
+ $this->interpolate($array[$key], $replacements);
+ continue;
+ }
+ foreach ($replacements as $oldVal => $newVal) {
+ if ($array[$key] === $oldVal) {
+ $array[$key] = $newVal;
+ }
+ }
+ }
+ }
+
+}
'description' => NULL,
'help_text' => NULL,
'on_change' => array(
- array('CRM_Case_Info', 'onToggleComponents'),
- array('CRM_Core_Component', 'flushEnabledComponents'),
+ 'CRM_Case_Info::onToggleComponents',
+ 'CRM_Core_Component::flushEnabledComponents',
+ 'call://resources/resetCacheCode',
),
),
'disable_core_css' => array(
--- /dev/null
+<?php
+
+namespace Civi\Core {
+ require_once 'CiviTest/CiviUnitTestCase.php';
+
+ /**
+ * Class ResolverTest
+ * @package Civi\Core
+ */
+ class ResolverTest extends \CiviUnitTestCase {
+
+ /**
+ * @var Resolver
+ */
+ protected $resolver;
+
+ /**
+ * Test setup.
+ */
+ protected function setUp() {
+ parent::setUp(); // TODO: Change the autogenerated stub
+ $this->resolver = new Resolver();
+ }
+
+ /**
+ * Test callback with a constant value.
+ */
+ public function testConstant() {
+ $cb = $this->resolver->get('0');
+ $actual = call_user_func($cb, 'foo');
+ $this->assertTrue(0 === $actual);
+
+ $cb = $this->resolver->get('1');
+ $actual = call_user_func($cb, 'foo');
+ $this->assertTrue(1 === $actual);
+ }
+
+ /**
+ * Test callback for a global function.
+ */
+ public function testGlobalFunc() {
+ // Note: civi_core_callback_dummy is implemented at the bottom of this file.
+ $cb = $this->resolver->get('civi_core_callback_dummy');
+ $this->assertEquals('civi_core_callback_dummy', $cb);
+
+ $expected = 'global dummy received foo';
+ $actual = call_user_func($cb, 'foo');
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test callback for a static function.
+ */
+ public function testStatic() {
+ $cb = $this->resolver->get('Civi\Core\ResolverTest::dummy');
+ $this->assertEquals(array('Civi\Core\ResolverTest', 'dummy'), $cb);
+
+ $expected = 'static dummy received foo';
+ $actual = call_user_func($cb, 'foo');
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test callback for an API.
+ */
+ public function testApi3() {
+ // Note: The Resolvertest.Ping API is implemented at the bottom of this file.
+ $cb = $this->resolver->get('api3://Resolvertest/ping?first=@1');
+ $expected = 'api dummy received foo';
+ $actual = call_user_func($cb, 'foo');
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test callback for an object in the container.
+ */
+ public function testCall() {
+ // Note: ResolverTestExampleService is implemented at the bottom of this file.
+ Container::singleton()->set('callbackTestService', new ResolverTestExampleService());
+ $cb = $this->resolver->get('call://callbackTestService/ping');
+ $expected = 'service dummy received foo';
+ $actual = call_user_func($cb, 'foo');
+ $this->assertEquals($expected, $actual);
+ }
+
+ /**
+ * Test callback for an invalid object in the container.
+ *
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\ExceptionInterface
+ */
+ public function testCallWithInvalidService() {
+ $this->resolver->get('call://totallyNonexistentService/ping');
+ }
+
+ /**
+ * Test object-lookup in the container.
+ */
+ public function testObj() {
+ // Note: ResolverTestExampleService is implemented at the bottom of this file.
+ Container::singleton()->set('callbackTestService', new ResolverTestExampleService());
+ $obj = $this->resolver->get('obj://callbackTestService');
+ $this->assertTrue($obj instanceof ResolverTestExampleService);
+ }
+
+ /**
+ * Test object-lookup in the container (invalid name).
+ *
+ * @expectedException \Symfony\Component\DependencyInjection\Exception\ExceptionInterface
+ */
+ public function testObjWithInvalidService() {
+ $this->resolver->get('obj://totallyNonexistentService');
+ }
+
+ /**
+ * Test default object creation.
+ */
+ public function testClass() {
+ // Note: ResolverTestExampleService is implemented at the bottom of this file.
+ $obj = $this->resolver->get('Civi\Core\ResolverTestExampleService');
+ $this->assertTrue($obj instanceof ResolverTestExampleService);
+ }
+
+ /**
+ * @param string $arg1
+ * Dummy value to pass through.
+ * @return array
+ */
+ public static function dummy($arg1) {
+ return "static dummy received $arg1";
+ }
+
+ }
+
+ /**
+ * Class ResolverTestExampleService
+ *
+ * @package Civi\Core
+ */
+ class ResolverTestExampleService {
+
+ /**
+ * @param string $arg1
+ * Dummy value to pass through.
+ * @return string
+ */
+ public function ping($arg1) {
+ return "service dummy received $arg1";
+ }
+
+ }
+}
+
+namespace {
+ /**
+ * @param string $arg1
+ * Dummy value to pass through.
+ * @return string
+ */
+ function civi_core_callback_dummy($arg1) {
+ return "global dummy received $arg1";
+ }
+
+ /**
+ * @param array $params
+ * API parameters.
+ * @return array
+ */
+ function civicrm_api3_resolvertest_ping($params) {
+ return civicrm_api3_create_success("api dummy received " . $params['first']);
+ }
+}