Dynamically fill arguments for single service

This commit is contained in:
John Paul E. Balandan, CPA 2020-11-05 21:10:03 +08:00
parent d304522912
commit fd25af189c
No known key found for this signature in database
GPG Key ID: FB7B51499BC27610
5 changed files with 196 additions and 83 deletions

View File

@ -1036,7 +1036,6 @@ if (! function_exists('service'))
if (! function_exists('single_service'))
{
/**
* Allow cleaner access to a Service.
* Always returns a new instance of the class.
*
* @param string $name
@ -1046,10 +1045,36 @@ if (! function_exists('single_service'))
*/
function single_service(string $name, ...$params)
{
// Ensure it's NOT a shared instance
array_push($params, false);
$service = Services::serviceExists($name);
return Services::$name(...$params);
if ($service === null)
{
// The service is not defined anywhere so just return.
return null;
}
$method = new ReflectionMethod($service, $name);
$count = $method->getNumberOfParameters();
$mParam = $method->getParameters();
$params = $params ?? [];
if ($count === 1)
{
// This service needs only one argument, which is the shared
// instance flag, so let's wrap up and pass false here.
return $service::$name(false);
}
// Fill in the params with the defaults, but stop before the last
for ($startIndex = count($params); $startIndex <= $count - 2; $startIndex++)
{
$params[$startIndex] = $mParam[$startIndex]->getDefaultValue();
}
// Ensure the last argument will not create a shared instance
$params[$count - 1] = false;
return $service::$name(...$params);
}
}

View File

@ -65,7 +65,12 @@ class BaseService
*/
protected static $services = [];
//--------------------------------------------------------------------
/**
* A cache of the names of services classes found.
*
* @var array<string>
*/
private static $serviceNames = [];
/**
* Returns a shared instance of any of the class' services.
@ -98,8 +103,6 @@ class BaseService
return static::$instances[$key];
}
//--------------------------------------------------------------------
/**
* The Autoloader class is the central class that handles our
* spl_autoload_register method, and helper methods.
@ -123,8 +126,6 @@ class BaseService
return new Autoloader();
}
//--------------------------------------------------------------------
/**
* The file locator provides utility methods for looking for non-classes
* within namespaced folders, as well as convenience methods for
@ -149,8 +150,6 @@ class BaseService
return new FileLocator(static::autoloader());
}
//--------------------------------------------------------------------
/**
* Provides the ability to perform case-insensitive calling of service
* names.
@ -162,17 +161,40 @@ class BaseService
*/
public static function __callStatic(string $name, array $arguments)
{
$name = strtolower($name);
$service = static::serviceExists($name);
if (method_exists(Services::class, $name))
if ($service === null)
{
return Services::$name(...$arguments);
return null;
}
return static::discoverServices($name, $arguments);
return $service::$name(...$arguments);
}
//--------------------------------------------------------------------
/**
* Check if the requested service is defined and return the declaring
* class. Return null if not found.
*
* @param string $name
*
* @return string|null
*/
public static function serviceExists(string $name): ?string
{
static::buildServicesCache();
$services = array_merge([Services::class], self::$serviceNames);
$name = strtolower($name);
foreach ($services as $service)
{
if (method_exists($service, $name))
{
return $service;
}
}
return null;
}
/**
* Reset shared instances and mocks for testing.
@ -181,8 +203,7 @@ class BaseService
*/
public static function reset(bool $initAutoloader = false)
{
static::$mocks = [];
static::$mocks = [];
static::$instances = [];
if ($initAutoloader)
@ -191,8 +212,6 @@ class BaseService
}
}
//--------------------------------------------------------------------
/**
* Inject mock object for testing.
*
@ -204,8 +223,6 @@ class BaseService
static::$mocks[strtolower($name)] = $mock;
}
//--------------------------------------------------------------------
/**
* Will scan all psr4 namespaces registered with system to look
* for new Config\Services files. Caches a copy of each one, then
@ -216,6 +233,10 @@ class BaseService
* @param array $arguments
*
* @return mixed
*
* @deprecated
*
* @codeCoverageIgnore
*/
protected static function discoverServices(string $name, array $arguments)
{
@ -266,4 +287,32 @@ class BaseService
return null;
}
protected static function buildServicesCache(): void
{
if (! static::$discovered)
{
$config = config('Modules');
if ($config->shouldDiscover('services'))
{
$locator = static::locator();
$files = $locator->search('Config/Services');
// Get instances of all service classes and cache them locally.
foreach ($files as $file)
{
$classname = $locator->getClassname($file);
if ($classname !== 'CodeIgniter\\Config\\Services')
{
self::$serviceNames[] = $classname;
static::$services[] = new $classname();
}
}
}
static::$discovered = true;
}
}
}

View File

@ -7,6 +7,7 @@ use CodeIgniter\HTTP\URI;
use CodeIgniter\HTTP\UserAgent;
use CodeIgniter\Router\RouteCollection;
use CodeIgniter\Session\Handlers\FileHandler;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\Mock\MockIncomingRequest;
use CodeIgniter\Test\Mock\MockSession;
use CodeIgniter\Test\TestLogger;
@ -18,11 +19,8 @@ use Tests\Support\Models\JobModel;
/**
* @backupGlobals enabled
*/
class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
class CommonFunctionsTest extends CIUnitTestCase
{
//--------------------------------------------------------------------
protected function setUp(): void
{
parent::setUp();
@ -31,8 +29,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
unset($_ENV['foo'], $_SERVER['foo']);
}
//--------------------------------------------------------------------
public function testStringifyAttributes()
{
$this->assertEquals(' class="foo" id="bar"', stringify_attributes(['class' => 'foo', 'id' => 'bar']));
@ -50,8 +46,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('', stringify_attributes([]));
}
// ------------------------------------------------------------------------
public function testStringifyJsAttributes()
{
$this->assertEquals('width=800,height=600', stringify_attributes(['width' => '800', 'height' => '600'], true));
@ -62,8 +56,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('width=800,height=600', stringify_attributes($atts, true));
}
// ------------------------------------------------------------------------
public function testEnvReturnsDefault()
{
$this->assertEquals('baz', env('foo', 'baz'));
@ -96,8 +88,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertNull(env('p4'));
}
// ------------------------------------------------------------------------
public function testRedirectReturnsRedirectResponse()
{
$_SERVER['REQUEST_METHOD'] = 'GET';
@ -122,8 +112,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertInstanceOf(\CodeIgniter\HTTP\RedirectResponse::class, redirect());
}
// ------------------------------------------------------------------------
public function testView()
{
$data = [
@ -145,16 +133,12 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertStringContainsString($expected, view('\Tests\Support\View\Views\simple'));
}
// ------------------------------------------------------------------------
public function testViewCell()
{
$expected = 'Hello';
$this->assertEquals($expected, view_cell('\Tests\Support\View\SampleClass::hello'));
}
// ------------------------------------------------------------------------
public function testEscapeWithDifferentEncodings()
{
$this->assertEquals('&lt;x', esc('<x', 'html', 'utf-8'));
@ -162,16 +146,12 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('&lt;x', esc('<x', 'html', 'windows-1251'));
}
// ------------------------------------------------------------------------
public function testEscapeBadContext()
{
$this->expectException(InvalidArgumentException::class);
esc(['width' => '800', 'height' => '600'], 'bogus');
}
// ------------------------------------------------------------------------
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
@ -208,17 +188,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals(null, session('notbogus'));
}
// ------------------------------------------------------------------------
public function testSingleService()
{
$timer1 = single_service('timer');
$timer2 = single_service('timer');
$this->assertFalse($timer1 === $timer2);
}
// ------------------------------------------------------------------------
public function testRouteTo()
{
// prime the pump
@ -228,8 +197,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('/path/string/to/13', route_to('myController::goto', 'string', 13));
}
// ------------------------------------------------------------------------
public function testInvisible()
{
$this->assertEquals('Javascript', remove_invisible_characters("Java\0script"));
@ -240,15 +207,11 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('Javascript', remove_invisible_characters('Java%0cscript', true));
}
// ------------------------------------------------------------------------
public function testAppTimezone()
{
$this->assertEquals('America/Chicago', app_timezone());
}
// ------------------------------------------------------------------------
public function testCSRFToken()
{
$this->assertEquals('csrf_test_name', csrf_token());
@ -274,8 +237,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertStringContainsString('<meta name="X-CSRF-TOKEN" ', csrf_meta());
}
// ------------------------------------------------------------------------
public function testModelNotExists()
{
$this->assertNull(model(UnexsistenceClass::class));
@ -296,8 +257,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertInstanceOf(JobModel::class, model('\Tests\Support\Models\JobModel'));
}
// ------------------------------------------------------------------------
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
@ -370,16 +329,12 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals($locations, old('location'));
}
// ------------------------------------------------------------------------
public function testReallyWritable()
{
// cannot test fully on *nix
$this->assertTrue(is_really_writable(WRITEPATH));
}
// ------------------------------------------------------------------------
public function testSlashItem()
{
$this->assertEquals('/', slash_item('cookiePath')); // slash already there
@ -415,7 +370,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
\CodeIgniter\Config\BaseService::injectMock('session', $session);
}
//--------------------------------------------------------------------
// Make sure cookies are set by RedirectResponse this way
// See https://github.com/codeigniter4/CodeIgniter4/issues/1393
public function testRedirectResponseCookies1()
@ -435,8 +389,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertTrue($answer1->hasCookie('login_time'));
}
//--------------------------------------------------------------------
public function testTrace()
{
ob_start();
@ -456,8 +408,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertStringContainsString('<h1>is_not</h1>', view('\Tests\Support\View\Views\simples'));
}
//--------------------------------------------------------------------
/**
* @runInSeparateProcess
* @preserveGlobalState disabled
@ -471,8 +421,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
$this->assertEquals('https://example.com/', Services::response()->getHeader('Location')->getValue());
}
//--------------------------------------------------------------------
/**
* @dataProvider dirtyPathsProvider
*/
@ -509,8 +457,6 @@ class CommonFunctionsTest extends \CodeIgniter\Test\CIUnitTestCase
];
}
//--------------------------------------------------------------------
public function testHelperWithFatalLocatorThrowsException()
{
// Replace the locator with one that will fail if it is called

View File

@ -0,0 +1,96 @@
<?php
use CodeIgniter\Config\Services;
use CodeIgniter\Test\CIUnitTestCase;
final class CommonSingleServiceTest extends CIUnitTestCase
{
/**
* @dataProvider serviceNamesProvider
*
* @param string $service
*
* @return void
*/
public function testSingleServiceWithNoParamsSupplied(string $service): void
{
$service1 = single_service($service);
$service2 = single_service($service);
$this->assertSame(get_class($service1), get_class($service2));
$this->assertNotSame($service1, $service2);
}
/**
* @dataProvider serviceNamesProvider
*
* @param string $service
*
* @return void
*/
public function testSingleServiceWithAtLeastOneParamSupplied(string $service): void
{
$params = [];
$method = new ReflectionMethod(Services::class, $service);
if ($method->getNumberOfParameters() === 1)
{
$params[] = true;
}
else
{
$params[] = $method->getParameters()[0]->getDefaultValue();
}
$service1 = single_service($service, ...$params);
$service2 = single_service($service, ...$params);
$this->assertSame(get_class($service1), get_class($service2));
$this->assertNotSame($service1, $service2);
}
public function testSingleServiceWithAllParamsSupplied(): void
{
$cache1 = single_service('cache', null, true);
$cache2 = single_service('cache', null, true);
// Assert that even passing true as last param this will
// not create a shared instance.
$this->assertSame(get_class($cache1), get_class($cache2));
$this->assertNotSame($cache1, $cache2);
}
public function testSingleServiceWithGibberishGiven(): void
{
$this->assertNull(single_service('foo'));
$this->assertNull(single_service('bar'));
$this->assertNull(single_service('baz'));
$this->assertNull(single_service('caches'));
$this->assertNull(single_service('timers'));
}
public static function serviceNamesProvider(): iterable
{
$methods = (new ReflectionClass(Services::class))->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method)
{
$name = $method->getName();
$excl = [
'__callStatic',
'serviceExists',
'reset',
'injectMock',
'encrypter', // Encrypter needs a starter key
'session', // Headers already sent
];
if (in_array($name, $excl, true))
{
continue;
}
yield [$name];
}
}
}

View File

@ -8,7 +8,6 @@ use CodeIgniter\Test\Mock\MockResponse;
class ServicesTest extends CIUnitTestCase
{
protected $config;
protected $original;
@ -17,15 +16,13 @@ class ServicesTest extends CIUnitTestCase
parent::setUp();
$this->original = $_SERVER;
// $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'es; q=1.0, en; q=0.5';
$this->config = new App();
// $this->config->negotiateLocale = true;
// $this->config->supportedLocales = ['en', 'es'];
$this->config = new App();
}
public function tearDown(): void
{
$_SERVER = $this->original;
Services::reset();
}
public function testNewAutoloader()