Slim framework v4 -- Add globals to TwigView - php

In testing, I have been unable to add globals to Slim v4's TwigView. It used to be that you could do it like so:
$twigView->getEnvironment()->addGlobal('flash', $container->get('flash'));
$twigView->getEnvironment()->addGlobal('session', $_SESSION);
But that now throws the exception: Unable to add global 'flash' as the runtime or the extensions have already been initialized.
I took a look at the Environment class of Twig and found this bit of validation:
/**
* Registers a Global.
*
* New globals can be added before compiling or rendering a template;
* but after, you can only update existing globals.
*
* #param string $name The global name
* #param mixed $value The global value
*/
public function addGlobal($name, $value)
{
if ($this->extensionSet->isInitialized() && !array_key_exists($name, $this->getGlobals())) {
throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
}
if (null !== $this->resolvedGlobals) {
$this->resolvedGlobals[$name] = $value;
} else {
$this->globals[$name] = $value;
}
}
Can anybody explain to me why we seem to be throwing the exception if the global DOESN'T exist, instead of if it DOES exist? That seems to be a logic error to me, but perhaps I'm misunderstanding it.
Thanks in advance.

It is not a logical error.
From the DocBlock you posted with your question:
New globals can be added before compiling or rendering a template; but after, you can only update existing globals.
Now having a look at the code:
if (
// if the extension is initialized, the right side of && operator will be evaluated and...
$this->extensionSet->isInitialized() &&
// ...this means, we are only allowed to UPDATE a global,
// so the global should already exist
// and its lack of existence is an error, hence, if it does not exists, we throw an exception
!array_key_exists($name, $this->getGlobals())
) {
throw new \LogicException(sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name));
}
The error text basically says:
A global with the name you're referring to does not exist, which means this is not an update but a creation operation,
and since the extension is already initialized, you're not allowed to do an update.

Related

Why Codeignitor does not accept autoload Controller classes when validating routes?

Why does Codeignitor not accept Controller in composer autoload when validating routes?
It's checking by: class_exists($class, FALSE) where the second parameter disables checking in autoload.
https://github.com/bcit-ci/CodeIgniter
$e404 = FALSE;
$class = ucfirst($RTR->class);
$method = $RTR->method;
if (empty($class) OR ! file_exists(APPPATH.'controllers/'.$RTR->directory.$class.'.php'))
{
$e404 = TRUE;
}
else
{
require_once(APPPATH.'controllers/'.$RTR->directory.$class.'.php');
if ( ! class_exists($class, FALSE) OR $method[0] === '_' OR method_exists('CI_Controller', $method))
{
$e404 = TRUE;
}
elseif (method_exists($class, '_remap'))
{
$params = array($method, array_slice($URI->rsegments, 2));
$method = '_remap';
}
elseif ( ! method_exists($class, $method))
{
$e404 = TRUE;
}
/**
* DO NOT CHANGE THIS, NOTHING ELSE WORKS!
*
* - method_exists() returns true for non-public methods, which passes the previous elseif
* - is_callable() returns false for PHP 4-style constructors, even if there's a __construct()
* - method_exists($class, '__construct') won't work because CI_Controller::__construct() is inherited
* - People will only complain if this doesn't work, even though it is documented that it shouldn't.
*
* ReflectionMethod::isConstructor() is the ONLY reliable check,
* knowing which method will be executed as a constructor.
*/
elseif ( ! is_callable(array($class, $method)))
{
$reflection = new ReflectionMethod($class, $method);
if ( ! $reflection->isPublic() OR $reflection->isConstructor())
{
$e404 = TRUE;
}
}
}
Looking over the git history, the change was introduced in 49e68de96b420a444c826995746a5f09470e76d9, with the commit message being:
Disable autoloader call from class_exists() occurences to improve performance
Note: The Driver libary tests seem to depend on that, so one occurence in CI_Loader is left until we resolve that.
So the nominal reason is performance.
If you want to ensure that the controller classes will be loaded on each request, you can add the files explicitly to the Composer autoload.files attribute, like so:
composer.json
{
"autoload": {
"files": [
"src/Foo.php"
]
},
"name": "test/64166739"
}
src/Foo.php
<?php
class Foo {}
test.php
<?php
$loader = require('./vendor/autoload.php');
var_dump(class_exists('Foo', false));
When run (via php test.php for example), we get the following output:
bool(true)
Additional
Looking over the code around that call to class_exists, it would appear that the controller files should follow a convention such that, for example with the built in Welcome controller and the default settings, the file that defines it should exist at:
application/controllers/Welcome.php
and so after require_onceing that file, the call to class_exists is a reasonably simple sanity check to ensure that the file did in fact define that class. So, based on this assumption about how controllers are added to the CodeIgniter application (ie all in the application/controllers directory and named the same as the class that they define), it's reasonable to bypass the autoloader when performing that check.
If you wanted to ensure the controllers are loaded when needed, the CodeIgniter way, they should be added to the application as listed above.

PHP returns object as null occasionally and unintentionally

We have PHP code in production that sometimes fails with "Call to member function on null", although the same code path executes fine several times before that in one invocation. We have a test that reproduces the error consistently at the same run of the loop.
I already proved that the object gets created correctly in the factory even if it gets returned as null. The factory method must not return null in any case, as indicated in the DocBlock. This question is not related to nullable return types or something like that.
The process does not exceed memory or runtime limitations and I already tried turning off the garbage collector, but no luck. The error happens both in PHP 7.0 and 7.3 on Debian, did not try on other versions or operating systems.
I am not allowed to paste the real code here, but I wrote a simple mockup to explain in more detail. Please keep in mind that this demo code will not result in the error, it is just meant to show the general structure of the program that runs into this fault.
// Three placeholder classes with common methods
class Bender
{
public function common()
{
echo "Bend, bend!" . PHP_EOL;
}
}
class Clamper
{
public function common()
{
echo "Clamp, clamp!" . PHP_EOL;
}
}
class Worker
{
public function common()
{
echo "Work, work!" . PHP_EOL;
}
}
// abstract class with static factory to produce objects
abstract class MomCorp
{
/**
* Factory to create one of several objects
*
* #param string $name
* #return Bender|Clamper|Worker
*/
public static function factory($name)
{
$type = self::managementDecision($name);
switch ($type)
{
case "bender":
$robot = new Bender();
break;
case "clamper":
$robot = new Clamper();
break;
default:
$robot = new Worker();
}
// optional QA works flawlessly here, object is fine all the time!
// $robot->common();
return $robot;
}
public static function managementDecision($name)
{
// irrelevant magic happens on $name here
return "bender";
}
}
foreach (['Rodriguez', 'Angle-ine', 'Flexo', 'Billie'] as $newName)
{
echo "$newName: ";
// The next two lines break after some loops - why?
// The perfectly functional object in factory gets returned as null
$bot = MomCorp::factory($newName);
$bot->common();
}
// SAMPLE OUTPUT
// Rodriguez: Bend, bend!
// Angle-ine: Bend, bend!
// Flexo: Bend, bend!
// Billie: Call to a member function common() on null
Has anyone experienced the same and has any hints on what might cause such an error and how to fix it?

Getting twig partial markup in controller throws error?

I'm trying to get rendered twig content in a plugin controller. I'm using CmsCompoundObject to load a partial but it's throwing an error.
$theme = Theme::getActiveThemeCode();
$markup = CmsCompoundObject::load($theme, "partials/test.htm")->getTwigContent();
The from property is invalid, make sure that Cms\Classes\CmsCompoundObject has a string value for its $dirName property (use '' if not using directories)
../october/rain/src/Halcyon/Builder.php line 309
Looking at the code, this is where it's throwing the error:
/**
* #return array
*/
protected function runSelect()
{
if (!is_string($this->from)) {
throw new ApplicationException(sprintf("The from property is invalid, make sure that %s has a string value for its \$dirName property (use '' if not using directories)", get_class($this->model)));
}
if ($this->selectSingle) {
list($name, $extension) = $this->selectSingle;
return $this->datasource->selectOne($this->from, $name, $extension);
}
}
$theme is a string and not empty, it properly gets the current theme so I'm not sure why it's saying invalid.
OctoberCMS Slack channel helped me find the solution.
First was to use Partial instead of CmsCompoundObject and the 2nd was to remove "partials/" from the path name as the Partial class already knows to look in that directory.
$theme = Theme::getActiveTheme();
$markup = Partial::load($theme, "test.htm")->getTwigContent();

Creating a directory from multiple threads

If multiple threads do the following:
if ( !is_dir($dir) )
mkdir($dir, 0, true);
what will happen if two threads will detect "at the same time" that the directory does not exist and then they both try and create it?
Is mkdir synchronized to prevent bad things from happening or is there a way to flock this to make sure that only one thread creates the directory and/or files ?
I know this an old question but something like this works really well, assuming you have an error handler that converts errors to exceptions (which is a good idea anyway):
/**
* Create a folder in a thread safe way
* Between 'is_dir' and 'mkdir' another thread could have created a folder.
* This can cause the system to raise an unwarrented error
*
* Returns TRUE if folder was created, NULL if folder already exists
*
* Throws exception on any other error
*
* #param array $array
* #param string $key
* #param mixed $value
* #return array
*/
function mkdir_thread_safe($dir, $permissions = 0777, $recursive = false)
{
if(is_dir($dir)) return;
try {
mkdir($dir, $permissions, $recursive);
}
catch (\ErrorException $e){
if(is_dir($dir)) return;
throw $e;
}
return true;
}
Here is the error to exception conversion handler - this is really good practice anyway, and should be added at the start of any application entry point. Some modern frameworks such as laravel, take care of this by default, so if you are using laravel, your errors will already be converted to exceptions.
set_error_handler(function ($level, $message, $file, $line, $context)
{
if (error_reporting() & $level)
{
throw new ErrorException($message, $level, 0, $file, $line);
}
});
Only one of them will manage to create the directory, the other mkdir will return false and throw a warning
You can also have a look at this bug in php, it's not exactly the case like with your question, but it is related

Fast check if an object will be successfully instantiated in PHP?

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
}
}

Categories