1: <?php
2:
3: declare(strict_types=1);
4:
5: namespace Atk4\Core;
6:
7: class Factory
8: {
9: use WarnDynamicPropertyTrait;
10:
11: private static ?Factory $_instance = null;
12:
13: protected function __construct()
14: {
15: // singleton
16: }
17:
18: final protected static function getInstance(): self
19: {
20: if (self::$_instance === null) {
21: self::$_instance = new self();
22: }
23:
24: return self::$_instance;
25: }
26:
27: /**
28: * @param array<mixed>|object|null ...$seeds
29: *
30: * @return array<mixed>|object
31: */
32: protected function _mergeSeeds(...$seeds)
33: {
34: // merge seeds but prefer seed over seed2
35: // move numerical keys to the beginning and sort them
36: $arguments = [];
37: $injection = [];
38: $obj = null;
39: $beforeObjKeys = null;
40: foreach ($seeds as $seedIndex => $seed) {
41: if (is_object($seed)) {
42: if ($obj !== null) {
43: throw new Exception('Two or more objects specified as seed');
44: }
45:
46: $obj = $seed;
47: if (count($injection) > 0) {
48: $beforeObjKeys = array_flip(array_keys($injection));
49: }
50:
51: continue;
52: } elseif ($seed === null) {
53: continue;
54: }
55:
56: // check seed
57: if (!array_key_exists(0, $seed)) {
58: // allow this method to be used to merge seeds without class name
59: } elseif ($seed[0] === null) {
60: // pass
61: } elseif (!is_string($seed[0])) {
62: throw new Exception('Seed class type (' . get_debug_type($seed[0]) . ') must be string');
63: } /*elseif (!class_exists($seed[0])) {
64: throw new Exception('Seed class "' . $seed[0] . '" not found');
65: }*/
66:
67: foreach ($seed as $k => $v) {
68: if (is_int($k)) {
69: if (!isset($arguments[$k])) {
70: $arguments[$k] = $v;
71: }
72: } elseif ($v !== null) {
73: if (!isset($injection[$k])) {
74: $injection[$k] = $v;
75: }
76: }
77: }
78: }
79:
80: ksort($arguments, \SORT_NUMERIC);
81: if ($obj === null) {
82: $arguments += $injection;
83:
84: return $arguments;
85: }
86:
87: unset($arguments[0]); // the first argument specifies a class name
88: if (count($arguments) > 0) {
89: throw (new Exception('Constructor arguments cannot be injected into existing object'))
90: ->addMoreInfo('object', $obj)
91: ->addMoreInfo('arguments', $arguments);
92: }
93:
94: if (count($injection) > 0) {
95: if (!TraitUtil::hasDiContainerTrait($obj)) {
96: throw (new Exception('Property injection is possible only to objects that use Atk4\Core\DiContainerTrait trait'))
97: ->addMoreInfo('object', $obj)
98: ->addMoreInfo('injection', $injection);
99: }
100:
101: if ($beforeObjKeys !== null) {
102: $injectionActive = array_intersect_key($injection, $beforeObjKeys);
103: $injection = array_diff_key($injection, $beforeObjKeys);
104:
105: $obj->setDefaults($injectionActive, false);
106: }
107: $obj->setDefaults($injection, true);
108: }
109:
110: return $obj;
111: }
112:
113: /**
114: * @param class-string $className
115: * @param array<int, mixed> $ctorArgs
116: */
117: protected function _newObject(string $className, array $ctorArgs): object
118: {
119: return new $className(...$ctorArgs);
120: }
121:
122: /**
123: * @param array<mixed>|object $seed
124: * @param array<mixed> $defaults
125: */
126: protected function _factory($seed, array $defaults = null): object
127: {
128: if ($defaults === null) { // should be deprecated soon (with [] default value)
129: $defaults = [];
130: }
131:
132: if (!is_array($seed) && !is_object($seed)) { // @phpstan-ignore-line
133: throw new Exception('Use of non-array (' . gettype($seed) . ') seed is not supported');
134: }
135:
136: array_unshift($defaults, null); // insert argument 0
137:
138: if (is_object($seed)) {
139: $defaults = $this->_mergeSeeds([], $defaults);
140: $defaults[0] = $seed;
141: $seed = $defaults;
142: } else {
143: $seed = $this->_mergeSeeds($seed, $defaults);
144: }
145: unset($defaults);
146:
147: $arguments = array_filter($seed, 'is_int', \ARRAY_FILTER_USE_KEY); // with numeric keys
148: $injection = array_diff_key($seed, $arguments); // with string keys
149: $object = array_shift($arguments); // first numeric key argument is object
150:
151: if (!is_object($object)) {
152: if (!is_string($object)) {
153: throw (new Exception('Class name is not specified by the seed'))
154: ->addMoreInfo('seed', $seed);
155: }
156:
157: $object = $this->_newObject($object, $arguments);
158: }
159:
160: if (count($injection) > 0) {
161: $this->_mergeSeeds($injection, $object);
162: }
163:
164: return $object;
165: }
166:
167: /**
168: * Given two seeds (or more) will merge them, prioritizing the first argument.
169: * If object is passed on either of arguments, then it will setDefaults() remaining
170: * arguments, respecting their positioning.
171: *
172: * To learn more about mechanics of factory trait, see documentation
173: *
174: * @param array<mixed>|object|null ...$seeds
175: *
176: * @return object|array<mixed> if at least one seed is an object, will return object
177: */
178: final public static function mergeSeeds(...$seeds)
179: {
180: return self::getInstance()->_mergeSeeds(...$seeds);
181: }
182:
183: /**
184: * Given a Seed (see doc) as a first argument, will create object of a corresponding
185: * class, call constructor with numerical arguments of a seed and inject key/value
186: * arguments.
187: *
188: * Argument $defaults has the same effect as the seed, but will not contain the class.
189: * Class is always determined by seed, except if you pass object into defaults.
190: *
191: * To learn more about mechanics of factory trait, see documentation
192: *
193: * @param array<mixed>|object $seed
194: * @param array<mixed> $defaults
195: */
196: final public static function factory($seed, $defaults = []): object
197: {
198: if ('func_num_args'() > 2) { // prevent bad usage
199: throw new \Error('Too many method arguments');
200: }
201:
202: return self::getInstance()->_factory($seed, $defaults);
203: }
204: }
205: