1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core;
4:
5: use Salient\Contract\Core\BuilderInterface;
6: use Salient\Core\Concern\ChainableTrait;
7: use Salient\Core\Concern\ImmutableTrait;
8: use Salient\Core\Exception\InvalidDataException;
9: use Salient\Core\Reflection\ClassReflection;
10: use Salient\Core\Reflection\MethodReflection;
11: use Salient\Core\Reflection\ParameterIndex;
12: use Salient\Utility\Exception\InvalidArgumentTypeException;
13: use Salient\Utility\Str;
14: use Closure;
15:
16: /**
17: * @api
18: *
19: * @template TClass of object
20: *
21: * @implements BuilderInterface<TClass>
22: */
23: abstract class Builder implements BuilderInterface
24: {
25: use ChainableTrait;
26: use ImmutableTrait;
27:
28: /**
29: * Get the class to instantiate
30: *
31: * @return class-string<TClass>
32: */
33: abstract protected static function getService(): string;
34:
35: /**
36: * Get a static method to call on the service class to create an instance,
37: * or null to use the constructor
38: */
39: protected static function getStaticConstructor(): ?string
40: {
41: return null;
42: }
43:
44: /**
45: * Get methods to forward to a new instance of the service class
46: *
47: * @return string[]
48: */
49: protected static function getTerminators(): array
50: {
51: return [];
52: }
53:
54: /** @var class-string<TClass> */
55: private string $Service;
56: private ?string $StaticConstructor;
57: /** @var Closure(string, bool=): string */
58: private Closure $Normaliser;
59: /** @var array<string,true> */
60: private array $Terminators = [];
61: private ?ParameterIndex $ParameterIndex = null;
62: /** @var array<string,mixed> */
63: private array $Data = [];
64:
65: /**
66: * @api
67: */
68: final public function __construct()
69: {
70: $this->Service = static::getService();
71: $this->StaticConstructor = static::getStaticConstructor();
72:
73: $class = new ClassReflection($this->Service);
74:
75: $this->Normaliser = $class->getNormaliser()
76: ?? fn(string $name) => Str::camel($name);
77:
78: foreach (static::getTerminators() as $terminator) {
79: $this->Terminators[$terminator] = true;
80: $this->Terminators[($this->Normaliser)($terminator, false)] = true;
81: }
82:
83: /** @var MethodReflection|null $constructor */
84: $constructor = $this->StaticConstructor === null
85: ? $class->getConstructor()
86: : $class->getMethod($this->StaticConstructor);
87:
88: if ($constructor) {
89: $this->ParameterIndex = $constructor->getParameterIndex($this->Normaliser);
90: }
91: }
92:
93: /**
94: * @inheritDoc
95: */
96: final public static function create()
97: {
98: /** @var static<TClass> */
99: return new static();
100: }
101:
102: /**
103: * @inheritDoc
104: */
105: final public static function resolve($object)
106: {
107: if ($object instanceof static) {
108: $object = $object->build();
109: } elseif (!is_a($object, static::getService())) {
110: throw new InvalidArgumentTypeException(
111: 1,
112: 'object',
113: static::class . '|' . static::getService(),
114: $object,
115: );
116: }
117: return $object;
118: }
119:
120: /**
121: * Get a value applied to the builder
122: *
123: * @return mixed
124: */
125: final public function getB(string $name)
126: {
127: return $this->Data[($this->Normaliser)($name)] ?? null;
128: }
129:
130: /**
131: * Check if a value has been applied to the builder
132: */
133: final public function issetB(string $name): bool
134: {
135: return array_key_exists(($this->Normaliser)($name), $this->Data);
136: }
137:
138: /**
139: * Remove a value applied to the builder
140: *
141: * @return static
142: */
143: final public function unsetB(string $name)
144: {
145: $data = $this->Data;
146: unset($data[($this->Normaliser)($name)]);
147: return $this->with('Data', $data);
148: }
149:
150: /**
151: * @inheritDoc
152: */
153: final public function build()
154: {
155: $data = $this->Data;
156: if ($this->ParameterIndex) {
157: $args = $this->ParameterIndex->DefaultArguments;
158: $argCount = $this->ParameterIndex->RequiredArgumentCount;
159: foreach ($data as $name => $value) {
160: $_name = $this->ParameterIndex->Names[$name] ?? null;
161: if ($_name !== null) {
162: $pos = $this->ParameterIndex->Positions[$_name];
163: $argCount = max($argCount, $pos + 1);
164: if (isset($this->ParameterIndex->PassedByReference[$name])) {
165: $args[$pos] = &$data[$name];
166: } else {
167: $args[$pos] = $value;
168: }
169: unset($data[$name]);
170: }
171: }
172: if (count($args) > $argCount) {
173: $args = array_slice($args, 0, $argCount);
174: }
175: }
176: if ($data) {
177: throw new InvalidDataException(sprintf(
178: 'Cannot call %s::%s() with: %s',
179: $this->Service,
180: $this->StaticConstructor ?? '__construct',
181: implode(', ', array_keys($data)),
182: ));
183: }
184: return $this->StaticConstructor === null
185: ? new $this->Service(...($args ?? []))
186: : $this->Service::{$this->StaticConstructor}(...($args ?? []));
187: }
188:
189: /**
190: * @internal
191: *
192: * @param mixed[] $arguments
193: * @return static
194: */
195: final public function __call(string $name, array $arguments)
196: {
197: if (
198: ($this->Terminators[$name] ?? null)
199: || ($this->Terminators[($this->Normaliser)($name, false)] ?? null)
200: ) {
201: return $this->build()->{$name}(...$arguments);
202: }
203:
204: return $this->withValueB($name, $arguments ? $arguments[0] : true);
205: }
206:
207: /**
208: * @param mixed $value
209: * @return static
210: */
211: final protected function withValueB(string $name, $value)
212: {
213: $data = $this->Data;
214: $data[($this->Normaliser)($name)] = $value;
215: return $this->with('Data', $data);
216: }
217:
218: /**
219: * @template TValue
220: *
221: * @param TValue $variable
222: * @return static
223: */
224: final protected function withRefB(string $name, &$variable)
225: {
226: $data = $this->Data;
227: $data[($this->Normaliser)($name)] = &$variable;
228: return $this->with('Data', $data);
229: }
230: }
231: