Skip to content

Commit

Permalink
Add central/singleton Factory and deprecate FactoryTrait trait
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Jun 6, 2020
1 parent fae2776 commit 5da2218
Show file tree
Hide file tree
Showing 8 changed files with 389 additions and 302 deletions.
41 changes: 41 additions & 0 deletions src/Core.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace atk4\core;

final class Core
{
/** @var Factory */
private static $_factory;

private function __construct()
{
// zeroton
}

private static function getFactory(): Factory
{
if (self::$_factory === null) {
self::$_factory = new Factory();
}

return self::$_factory;
}

/**
* See \atk4\core\Factory::mergeSeeds().
*/
public static function mergeSeeds(...$seeds)
{
return static::getFactory()->mergeSeeds(...$seeds);
}

/**
* See \atk4\core\Factory::factory().
*/
public static function factory($seed, $defaults = [], /* remove in 2021-jun, catch passed but no longer supported prefix */...$extraArgs): object
{
return static::getFactory()->factory($seed, $defaults, ...$extraArgs);
}
}
94 changes: 88 additions & 6 deletions src/DIContainerTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,93 @@ protected function setMissingProperty($key, $value)
return $this;
}

throw new Exception([
'Property for specified object is not defined',
'object' => $this,
'property' => $key,
'value' => $value,
]);
throw (new Exception('Property for specified object is not defined'))
->addMoreInfo('object', $this)
->addMoreInfo('property', $key)
->addMoreInfo('value', $value)
;//->toError(\Error::class);
}

/**
* 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('object_class', get_class($object))
;//->toError(\TypeError::class);
}

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))
;//->toError(\TypeError::class);
}

$seed = [$seed];
}

if (!isset($seed[0])) {
throw (new Exception('Class name is not defined in seed'))
;//->toError(\TypeError::class);
}

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

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 $seed The first element specifies a class name, other element are seed
*
* @return static
*/
public static function fromSeed($seed = [], $defaults = [])// :static supported by PHP8+
{
$seed = static::_fromSeedPrecheck($seed, false);
$object = Core::factory($seed);

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 $seed The first element specifies a class name, other element are seed
*
* @return static
*/
public static function fromSeedUnsafe($seed = [], $defaults = [])// :self is too strict with unsafe behaviour
{
$seed = static::_fromSeedPrecheck($seed, true);
$object = Core::factory($seed);

return $object;
}
}
11 changes: 11 additions & 0 deletions src/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ public function __construct(
$this->trace2 = $trace;
}

/**
* @param string $class must be a subtype of \Error, otherwise use this exception directly
*/
public function toError(string $class): \Error
{
$errorException = new $class($this->getMessage(), $this->getCode(), $this->getPrevious());
$errorException->atkException = $this;

return $errorException;
}

/**
* Change message (subject) of a current exception. Primary use is
* for localization purposes.
Expand Down
186 changes: 186 additions & 0 deletions src/Factory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

namespace atk4\core;

class Factory
{
/**
* 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.
*
* See full documentation.
*
* @param array|object|mixed $seed
* @param array|object|mixed $seed2
* @param array $more_seeds
*
* @return object|array if at least one seed is an object, will return object
*/
public function mergeSeeds($seed, $seed2, ...$more_seeds)
{
// recursively merge extra seeds
if ($more_seeds) {
$seed2 = $this->mergeSeeds($seed2, ...$more_seeds);
}

if (is_object($seed)) {
if (is_array($seed2)) {
// set defaults but don't override existing properties
$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, true);
} else {
throw new Exception([
'factory() requested to passively inject some properties into existing object that does not use \atk4\core\DIContainerTrait',
'object' => $seed,
'injection' => $injection,
]);
}
}
}

return $seed;
}

if (is_object($seed2)) {
// seed is not object, and setDefaults will complain if it's not array
if (is_array($seed)) {
$arguments = array_filter($seed, 'is_numeric', ARRAY_FILTER_USE_KEY); // with numeric keys
$injection = array_diff_key($seed, $arguments); // with string keys
if ($injection) {
if (isset($seed2->_DIContainerTrait)) {
$seed2->setDefaults($injection);
} else {
throw new Exception([
'factory() requested to inject some properties into existing object that does not use \atk4\core\DIContainerTrait',
'object' => $seed2,
'injection' => $seed,
]);
}
}
}

return $seed2;
}

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

/**
* 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 mixed $seed
* @param array $defaults
*/
public function factory($seed, $defaults = []): object
{
if (func_num_args() > 2) { // prevent bad usage
throw new \Error('Too many method arguments');
}

if ($defaults === null) {
$defaults = [];
}

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

if (!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 was not specified by the seed',
'seed' => $seed,
]);
}

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

if ($injection) {
if (isset($object->_DIContainerTrait)) {
$object->setDefaults($injection);
} else {
throw new Exception([
'factory() could not inject properties into new object. It does not use \atk4\core\DIContainerTrait',
'object' => $object,
'seed' => $seed,
'injection' => $injection,
]);
}
}

return $object;
}
}
Loading

0 comments on commit 5da2218

Please sign in to comment.