Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add central/singleton Factory and deprecate FactoryTrait trait #221

Merged
merged 7 commits into from
Jun 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/DIContainerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,94 @@ protected function setMissingProperty($key, $value)
->addMoreInfo('property', $key)
->addMoreInfo('value', $value);
}

/**
* Return the argument and assert it is instance of current class.
*
* The best, typehinting-friendly, way to annotate object type if it not defined
* at method header or strong typing in method header can not be used.
*
* @return static
*/
public static function assertInstanceOf(object $object)// :static supported by PHP8+
{
if (!($object instanceof static)) {
throw (new Exception('Object is not an instance of static class'))
->addMoreInfo('static_class', static::class)
->addMoreInfo('object_class', get_class($object));
}

return $object;
}

private static function _fromSeedPrecheck($seed, bool $unsafe)// :self is too strict with unsafe behaviour
{
if (!is_object($seed)) {
if (!is_array($seed)) {
if (!is_string($seed)) { // allow string class name seed but prevent bad usage
throw (new Exception('Seed must be an array, a string class name (deprecated) or an object'))
->addMoreInfo('seed_type', gettype($seed));
}

$seed = [$seed];
}

if (!isset($seed[0])) {
throw (new Exception('Class name is not specified by the seed'))
->addMoreInfo('seed', $seed);
}

$cl = $seed[0];
if (!$unsafe && !is_a($cl, static::class, true)) {
throw (new Exception('Seed class is not a subtype of static class'))
->addMoreInfo('static_class', static::class)
->addMoreInfo('seed_class', $cl);
}
}

return $seed;
}

/**
* Create new object from seed and assert it is instance of current class.
*
* The best, typehinting-friendly, way to create an object if it should not be
* immediately added to a parent (otherwise use addTo() method).
*
* @param array|string|object $seed the first element specifies a class name, other elements are seed
*
* @return static
*/
public static function fromSeed($seed = [], $defaults = [])// :static supported by PHP8+
{
if (func_num_args() > 2) { // prevent bad usage
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can reduce code here by simpy calling

$object = static::fromSeedUnsafe($seed, $defaults);

static::assertInstanceOf($object);

return $object;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Can, but do not want - this way, assertInstanceOf is computed even before the object is even created, which implies stronger security! (as seed can be an user input)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I see the assertInstanceOf is called before the $object is returned only?
The rest of the code is the same.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ No! _fromSeedPrecheck is called BEFORE object is constructed. Be very carefull around this and test for this behaviour will be highly welcomed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR for test: #311

throw new \Error('Too many method arguments');
}

$seed = static::_fromSeedPrecheck($seed, false);
$object = Factory::factory($seed, $defaults);

static::assertInstanceOf($object);

return $object;
}

/**
* Same as fromSeed(), but the new object is not asserted to be an instance of this class.
*
* @param array|string|object $seed the first element specifies a class name, other elements are seed
*
* @return static
*/
public static function fromSeedUnsafe($seed = [], $defaults = [])// :self is too strict with unsafe behaviour
{
if (func_num_args() > 2) { // prevent bad usage
throw new \Error('Too many method arguments');
}

$seed = static::_fromSeedPrecheck($seed, true);
$object = Factory::factory($seed, $defaults);

return $object;
}
}
2 changes: 1 addition & 1 deletion src/ExceptionRenderer/JSON.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public function __toString(): string
$this->json = [
'success' => false,
'code' => $this->exception->getCode(),
'message' => 'Error during JSON renderer : ' . $this->exception->getMessage(),
'message' => 'Error during JSON renderer: ' . $this->exception->getMessage(),
// avoid translation
//'message' => $this->_($this->exception->getMessage()),
'title' => get_class($this->exception),
Expand Down
9 changes: 7 additions & 2 deletions src/ExceptionRenderer/RendererAbstract.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ public function __toString(): string
return $this->output;
} catch (\Throwable $e) {
// fallback if Exception occur in renderer
return get_class($this->exception) . ' [' . $this->exception->getCode() . '] Error: ' . $this->_($this->exception->getMessage());
return '!! ATK4 CORE ERROR - EXCEPTION RENDER FAILED: '
. get_class($this->exception)
. ($this->exception->getCode() !== 0 ? '(' . $this->exception->getCode() . ')' : '')
. ': ' . $this->_($this->exception->getMessage()) . ' !!';
}
}

Expand Down Expand Up @@ -168,7 +171,9 @@ public function _($message, array $parameters = [], string $domain = null, strin

protected function getVendorDirectory(): string
{
return realpath(dirname(__DIR__, 4) . '/');
$loaderFile = (new \ReflectionClass(\Composer\Autoload\ClassLoader::class))->getFileName();

return realpath(dirname($loaderFile, 2) . '/');
}

protected function makeRelativePath(string $path): string
Expand Down
179 changes: 179 additions & 0 deletions src/Factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

declare(strict_types=1);

namespace atk4\core;

class Factory
{
/** @var Factory */
private static $_instance;

protected function __construct()
{
// singleton
}

final protected static function getInstance(): self
{
if (self::$_instance === null) {
self::$_instance = new self();
}

return self::$_instance;
}

protected function _mergeSeeds($seed, $seed2, ...$more_seeds)
{
// recursively merge extra seeds
if (count($more_seeds) > 0) {
$seed2 = $this->_mergeSeeds($seed2, ...$more_seeds);
}

if (is_object($seed) || is_object($seed2)) {
if (is_object($seed)) {
$passively = true; // set defaults but don't override existing properties
} else {
$passively = false;
[$seed, $seed2] = [$seed2, $seed]; // swap seeds
}

if (is_array($seed2)) {
$arguments = array_filter($seed2, 'is_numeric', ARRAY_FILTER_USE_KEY); // with numeric keys
$injection = array_diff_key($seed2, $arguments); // with string keys
if ($injection) {
if (isset($seed->_DIContainerTrait)) {
$seed->setDefaults($injection, $passively);
} else {
throw (new Exception('Property injection is possible only to objects that use \atk4\core\DIContainerTrait trait'))
->addMoreInfo('object', $seed)
->addMoreInfo('injection', $injection)
->addMoreInfo('passively', $passively);
}
}
}

return $seed;
}

if (!is_array($seed)) {
$seed = [$seed];
}

if (!is_array($seed2)) {
$seed2 = [$seed2];
}

// merge seeds but prefer seed over seed2
// move numerical keys to the beginning and sort them
$res = [];
$res2 = [];
foreach ($seed as $k => $v) {
if (is_numeric($k)) {
$res[$k] = $v;
} elseif ($v !== null) {
$res2[$k] = $v;
}
}
foreach ($seed2 as $k => $v) {
if (is_numeric($k)) {
if (!isset($res[$k])) {
$res[$k] = $v;
}
} elseif ($v !== null) {
if (!isset($res2[$k])) {
$res2[$k] = $v;
}
}
}
ksort($res, SORT_NUMERIC);
$res = $res + $res2;

return $res;
}

protected function createNewObject(string $className, array $ctorArgs): object
{
return new $className(...$ctorArgs);
}

protected function _factory($seed, $defaults = []): object
{
if ($defaults === null) { // should be deprecated soon
$defaults = [];
}

if ($seed === null) { // should be deprecated soon
$seed = [];
} elseif (!is_array($seed)) {
$seed = [$seed];
}

if (is_array($defaults)) {
array_unshift($defaults, null); // insert argument 0
} elseif (!is_object($defaults)) {
$defaults = [null, $defaults];
}
$seed = $this->_mergeSeeds($seed, $defaults);

if (is_object($seed)) {
// setDefaults() already called in _mergeSeeds()

return $seed;
}

$arguments = array_filter($seed, 'is_numeric', ARRAY_FILTER_USE_KEY); // with numeric keys
$injection = array_diff_key($seed, $arguments); // with string keys
$object = array_shift($arguments); // first numeric key argument is object

if (!is_object($object)) {
if (!is_string($object)) {
throw (new Exception('Class name is not specified by the seed'))
->addMoreInfo('seed', $seed);
}

$object = $this->createNewObject($object, $arguments);
}

if ($injection) {
$this->_mergeSeeds($injection, $object);
}

return $object;
}

/**
* Given two seeds (or more) will merge them, prioritizing the first argument.
* If object is passed on either of arguments, then it will setDefaults() remaining
* arguments, respecting their positioning.
*
* To learn more about mechanics of factory trait, see documentation
*
* @return object|array if at least one seed is an object, will return object
*/
final public static function mergeSeeds(...$seeds)
{
return self::getInstance()->_mergeSeeds(...$seeds);
}

/**
* Given a Seed (see doc) as a first argument, will create object of a corresponding
* class, call constructor with numerical arguments of a seed and inject key/value
* arguments.
*
* Argument $defaults has the same effect as the seed, but will not contain the class.
* Class is always determined by seed, except if you pass object into defaults.
*
* To learn more about mechanics of factory trait, see documentation
*
* @param array $defaults
*/
final public static function factory($seed, $defaults = []): object
{
if (func_num_args() > 2) { // prevent bad usage
throw new \Error('Too many method arguments');
}

return self::getInstance()->_factory($seed, $defaults);
}
}
Loading