1: <?php declare(strict_types=1);
2:
3: namespace Salient\Core\Concern;
4:
5: use Salient\Contract\Core\FacadeAwareInterface;
6: use Salient\Contract\Core\FacadeInterface;
7: use LogicException;
8:
9: /**
10: * Implements FacadeAwareInterface by returning modified instances for use with
11: * and without a facade
12: *
13: * @see FacadeAwareInterface
14: *
15: * @api
16: *
17: * @template TFacade of FacadeInterface
18: */
19: trait HasFacade
20: {
21: /** @var class-string<TFacade>|null */
22: protected ?string $Facade = null;
23: /** @var static|null */
24: private ?self $InstanceWithoutFacade = null;
25: /** @var static|null */
26: private ?self $InstanceWithFacade = null;
27: private bool $HasTentativeFacade = false;
28:
29: /**
30: * @param class-string<TFacade> $facade
31: * @return static
32: */
33: final public function withFacade(string $facade)
34: {
35: if ($this->Facade === $facade) {
36: return $this;
37: }
38:
39: if ($this->Facade !== null) {
40: // @codeCoverageIgnoreStart
41: throw new LogicException(sprintf(
42: '%s already has facade %s',
43: static::class,
44: $this->Facade,
45: ));
46: // @codeCoverageIgnoreEnd
47: }
48:
49: // Revert to `$this->InstanceWithFacade` if `$facade` matches and
50: // `$this` hasn't been cloned in the meantime
51: if (
52: $this->InstanceWithoutFacade === $this
53: && $this->InstanceWithFacade->Facade === $facade
54: ) {
55: return $this->InstanceWithFacade;
56: }
57:
58: $this->HasTentativeFacade = true;
59:
60: $instance = clone $this;
61: $instance->Facade = $facade;
62: $instance->InstanceWithoutFacade = $this;
63: $instance->InstanceWithFacade = $instance;
64: $instance->HasTentativeFacade = false;
65:
66: $this->InstanceWithoutFacade = $this;
67: $this->InstanceWithFacade = $instance;
68: $this->HasTentativeFacade = false;
69:
70: return $instance;
71: }
72:
73: /**
74: * @param class-string<TFacade> $facade
75: * @return static
76: */
77: final public function withoutFacade(string $facade, bool $unloading)
78: {
79: if ($this->Facade !== $facade) {
80: // @codeCoverageIgnoreStart
81: throw new LogicException(sprintf(
82: '%s does not have facade %s',
83: static::class,
84: $facade,
85: ));
86: // @codeCoverageIgnoreEnd
87: }
88:
89: // Revert to `$this->InstanceWithoutFacade` if `$this` hasn't been
90: // cloned in the meantime
91: if ($this->InstanceWithFacade === $this && !$unloading) {
92: return $this->InstanceWithoutFacade;
93: }
94:
95: $this->HasTentativeFacade = true;
96:
97: $instance = clone $this;
98: $instance->Facade = null;
99: $instance->HasTentativeFacade = false;
100:
101: // Keep a copy of the cloned instance for future reuse if the facade is
102: // not being unloaded
103: if ($unloading) {
104: $instance->InstanceWithoutFacade = null;
105: $instance->InstanceWithFacade = null;
106: } else {
107: $instance->InstanceWithoutFacade = $instance;
108: $instance->InstanceWithFacade = $this;
109:
110: $this->InstanceWithoutFacade = $instance;
111: $this->InstanceWithFacade = $this;
112: }
113:
114: $this->HasTentativeFacade = false;
115:
116: return $instance;
117: }
118:
119: /**
120: * If the object is not the underlying instance of the facade it's being
121: * used with, update the facade
122: *
123: * This method can be used to keep a facade up-to-date with an immutable
124: * underlying instance, e.g. by calling it from `__clone()`.
125: */
126: final protected function updateFacade(): void
127: {
128: if ($this->Facade === null || $this->HasTentativeFacade) {
129: return;
130: }
131:
132: if (!$this->Facade::isLoaded()) {
133: // @codeCoverageIgnoreStart
134: throw new LogicException(sprintf(
135: '%s has unloaded facade %s',
136: static::class,
137: $this->Facade,
138: ));
139: // @codeCoverageIgnoreEnd
140: }
141:
142: $instance = $this->Facade::getInstance();
143: if ($instance instanceof self && $instance->InstanceWithFacade === $this) {
144: // @codeCoverageIgnoreStart
145: return;
146: // @codeCoverageIgnoreEnd
147: }
148:
149: $this->Facade::swap($this);
150: }
151: }
152: