I would like to be able to modify an array on a Pimple container, however, because the services are frozen by Pimple this seems to be impossible.
I have tried the extend() method on the container, however, due to my array not being an object I am unable to modify it.
$container = new Slim\Container();
$container['config'] = ['foo'=>'bar'];
// .... do some other stuff.
$container['config']['baz'] = 'Harry'; // throws an error regarding indirect modification
Using extend
$container = new Slim\Container();
$container['config'] = ['foo'=>'bar'];
$container->extend('config',function($config,$container){
$config['baz'] = 'Harry';
return $config;
});
// throws an error PHP Fatal error: Uncaught InvalidArgumentException: Identifier "config" does not contain an object definition.
Is there no way to modify a definition inside Pimple container? Currently I am passing around a $config array by reference prior to instantiating the container which is less than ideal!
Thanks in advance
You can do this by unsetting the container value first.
Pimple freezes values but will allow you to remove them.
$container = new Slim\Container();
$container['config'] = ['foo'=>'bar'];
unset($container['config']);
$container->extend('config',function($config,$container){
$config['baz'] = 'Harry';
return $config;
});
Sorry, it turns out that I can just wrap the $config in a function to achieve desired results.
$container = new Slim\Container();
$config = ['foo'=>'bar'];
$container['config'] = function($container) use($config){
return $config;
};
$container->extend('config',function($config,$container) {
$config['baz']='Harry';
return $config;
});
print_r($container['config']);
// foo=>bar, baz=>Harry
Related
The following three approaches produce the same effect. Which approach should be used? Are there times one should be used over another?
function getConfig() {
$config=parse_ini_file(__DIR__.'/../config.ini',true);
$config['directories']['base']=dirname(__DIR__);
return $config;
}
$c = new \Slim\Container(['settings' => [
'displayErrorDetails'=>true,
'addContentLengthHeader'=>false,
'determineRouteBeforeAppMiddleware'=>true,
'config'=>getConfig()
]]);
$config=getConfig();
$c['pdo'] = function ($c) use($config){
$db1 = $c['settings']['config']['mysql'];
$db2 = $c->get('settings')['config']['mysql'];
$db3 = $config['mysql'];
return new \PDO(/* ... */);
};
Side note. Is it recommended to use a separate settings property for any application settings as I did with "config" to reduce the chance of conflicts with Slim's internal settings (i.e. httpVersion, responseChunkSize, outputBuffering, determineRouteBeforeAppMiddleware, displayErrorDetails, addContentLengthHeader, routerCacheFile)?
The get all settings just call the get() method like this:
$settings = $container->get('settings');
Is it possible get the value of a route placeholder within a Slim container? I know I can access the placeholder by adding a third parameter to the request but I'd like to have it injected so I'm not assigning it on each request.
I've tried $app->getContainer('router') but I can't seem to find a method to actually pull the placeholder value.
Example:
$app = new Slim\App;
$c = $app->getContainer();
$c['Controller'] = function() {
$userId = // how do I get the route placeholder userId?
return new Controller($userId);
};
$app->get('/user/{userId}','Controller:getUserId');
class Controller {
public function __construct($userId) {
$this->userId = $userId;
}
public function getUserId($request,$response) {
return $response->withJson($this->userId);
}
}
Without some 'hacky' things this will not work because we have no access on the request object build by slim, while the controller get constructed. So I recommend you to just use the 3rd parameter and get your userid from there.
The 'hacky' thing would be todo the same, what slim does when you execute $app->run(), but if you really want todo this, here you'll go:
$c['Controller'] = function($c) {
$routeInfo = $c['router']->dispatch($c['request']);
$args = $routeInfo[2];
$userId = $args['userId'];
return new Controller($userId);
};
Note: slim3 also urldecoded this values so may do this as well urldecode($args['userId']) Source
create a container wrapper and a maincontroller then extend all your controller from your maincontroller, then you have access to the container.
here is how i solved this problem:
https://gist.github.com/boscho87/d5834ac1ba42aa3da02e905aa346ee30
I have a method, which takes a reference
// CarService.php
public function getCars(&$carCollection = null)
{
$promise = // guzzle request for getting all cars would be here
$promise->then(function (ResponseInterface $response) use (&$carCollection) {
$cars= json_decode($response->getBody(), true);
$carCollection= new CarCollection($cars);
});
}
However, when accessing the collection and trying to reuse it, I'm getting the error
Argument 1 passed to {placeholder} must be an instance of {placeholder}, null given
I know that the reason for this is, that the constructor returns nothing, but how can I still assign my variable to a new instance of the CarCollection (which extends Doctrine's ArrayCollection)
I even tried it with a static method as a work around
// CarCollection.php
public static function create(array $cars): CarCollection
{
$carCollection = new CarCollection($cars);
return $carCollection;
}
// CarService.php
public function getCars(&$carCollection = null)
{
$cars = // curl request for getting all cars would be here
$carCollection = CarCollection::create($cars)
}
but it's still null. Why is that? How can I set a referenced variable to a new class?
I access the method like this
$carService = $this->get('tzfrs.vehicle.services.car');
$carCollection = null;
$promises = [
$carService->getCars($carCollection)
];
\GuzzleHttp\Promise\unwrap($promises);
var_dump($carCollection); // null
When I set the reference directly, eg.
// CarService.php
public function getCars(&$carCollection = null)
{
$carCollection = new CarCollection([]);
}
it works without any problems. Seems like the callback is somehow the problem.
Whoever downvoted this, can you please elaborate why and why you voted to close?
I might be misunderstanding the question, but you should be able to modify an object when passing by reference. See here for an example: https://3v4l.org/KtFvZ
In the later example code that you added, you shouldn't pass $carCollection by reference, the & should only be in the method/function defintion, not provided when you call it. I don't think that is your problem though, that should be throwing an error in php7.
How can I pass a dynamic dependency from one registered container definition to another? In this case, a generic Database object wants to inherit from a generic Config object. The twist is config is not static, but loaded depending on a given environment variable.
Config pertinent methods
public function __construct()
{
$configFile = 'example.config.yml';
$yamlParser = new Parser();
$reader = new Config\Reader($yamlParser);
$configYaml = $reader->parse(file_get_contents($configFile));
$config = new Config\Environment(getenv('SITE'), $configYaml);
$this->config = $config;
}
public function getEnvironmentConfig()
{
return $this->config;
}
Registering config is as simple as
$container->register('config', 'Config');
Database is currently added to the container as follows:
$container
->register('database', 'Database')
->addArgument($config->getEnvironmentConfig('Database', 'db.username'))
->addArgument($config->getEnvironmentConfig('Database', 'db.password'))
;
But I want to do something like
$container
->register('database', 'Database')
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.username'))
->addArgument(new Reference('config')->getEnvironmentConfig('Database', 'db.password'))
;
The $config in-PHP variable makes migrating from a PHP-built config impossible. I want to define the services in yaml force the container to:
Instantiate Config
Parse the config yaml file and create an environment-specific version
Return this on a call to getEnvironmentConfig
Is this possible?
This was solved by using the Expression Language Component
So you can easily chain method calls, for example:
use Symfony\Component\ExpressionLanguage\Expression;
$container->register('database', 'Database')
->addArgument(new Expression('service("config").getEnvironmentConfig("Database", "db.username")'));
How can I check if an object will be successfully instantiated with the given argument, without actually creating the instance?
Actually I'm only checking (didn't tested this code, but should work fine...) the number of required parameters, ignoring types:
// Filter definition and arguments as per configuration
$filter = $container->getDefinition($serviceId);
$args = $activeFilters[$filterName];
// Check number of required arguments vs arguments in config
$constructor = $reflector->getConstructor();
$numRequired = $constructor->getNumberOfRequiredParameters();
$numSpecified = is_array($args) ? count($args) : 1;
if($numRequired < $numSpecified) {
throw new InvalidFilterDefinitionException(
$serviceId,
$numRequired,
$numSpecified
);
}
EDIT: $constructor can be null...
The short answer is that you simply cannot determine if a set of arguments will allow error-free instantiation of a constructor. As commenters have mentioned above, there's no way to know for sure if a class can be instantiated with a given argument list because there are runtime considerations that cannot be known without actually attempting
instantiation.
However, there is value in trying to instantiate a class from a list of constructor arguments. The most obvious use-case for this sort of operation is a configurable Dependency Injection Container (DIC). Unfortunately, this is a much more complicated operation than the OP suggests.
We need to determine for each argument in a supplied definition array whether or not it matches specified type-hints from the constructor method signature (if the method signature actually has type-hints). Also, we need to resolve how to treat default argument values. Additionally, for our code to be of any real use we need to allow the specification of "definitions" ahead of time for instantiating a class. A sophisticated treatment of the problem will also involve a pool of reflection objects (caching) to minimize the performance impact of repeatedly reflecting things.
Another hurdle is the fact that there's no way to access the type-hint of a reflected method parameter without calling its ReflectionParameter::getClass method and subsequently instantiating a reflection class from the returned class name (if null is returned the param has no type-hint). This is where caching generated reflections becomes particularly important for any real-world use-case.
The code below is a severely stripped-down version of my own string-based recursive dependency injection container. It's a mixture of pseudo-code and real-code (if you were hoping for free code to copy/paste you're out of luck). You'll see that the code below matches the associative array keys of "definition" arrays to the parameter names in the constructor signature.
The real code can be found over at the relevant github project page.
class Provider {
private $definitions;
public function define($class, array $definition) {
$class = strtolower($class);
$this->definitions[$class] = $definition;
}
public function make($class, array $definition = null) {
$class = strtolower($class);
if (is_null($definition) && isset($this->definitions[$class])) {
$definition = $this->definitions[$class];
}
$reflClass = new ReflectionClass($class);
$instanceArgs = $this->buildNewInstanceArgs($reflClass);
return $reflClass->newInstanceArgs($instanceArgs);
}
private function buildNewInstanceArgs(
ReflectionClass $reflClass,
array $definition
) {
$instanceArgs = array();
$reflCtor = $reflClass->getConstructor();
// IF no constructor exists we're done and should just
// return a new instance of $class:
// return $this->make($reflClass->name);
// otherwise ...
$reflCtorParams = $reflCtor->getParameters();
foreach ($reflCtorParams as $ctorParam) {
if (isset($definition[$ctorParam->name])) {
$instanceArgs[] = $this->make($definition[$ctorParam->name]);
continue;
}
$typeHint = $this->getParameterTypeHint($ctorParam);
if ($typeHint && $this->isInstantiable($typeHint)) {
// The typehint is instantiable, go ahead and make a new
// instance of it
$instanceArgs[] = $this->make($typeHint);
} elseif ($typeHint) {
// The typehint is abstract or an interface. We can't
// proceed because we already know we don't have a
// definition telling us which class to instantiate
throw Exception;
} elseif ($ctorParam->isDefaultValueAvailable()) {
// No typehint, try to use the default parameter value
$instanceArgs[] = $ctorParam->getDefaultValue();
} else {
// If all else fails, try passing in a NULL or something
$instanceArgs[] = NULL;
}
}
return $instanceArgs;
}
private function getParameterTypeHint(ReflectionParameter $param) {
// ... see the note about retrieving parameter typehints
// in the exposition ...
}
private function isInstantiable($class) {
// determine if the class typehint is abstract/interface
// RTM on reflection for how to do this
}
}