Skip to content

Commit e545df6

Browse files
committed
Introduce trait Properties
Was formerly part of [ipl-orm](https://github.com/Icinga/ipl-orm)
1 parent 0b27482 commit e545df6

File tree

3 files changed

+399
-0
lines changed

3 files changed

+399
-0
lines changed

src/Properties.php

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
<?php
2+
3+
namespace ipl\Stdlib;
4+
5+
use Closure;
6+
use OutOfBoundsException;
7+
8+
/**
9+
* Trait for property access, mutation and array access.
10+
*/
11+
trait Properties
12+
{
13+
/** @var array */
14+
protected $properties = [];
15+
16+
/** @var array */
17+
protected $mutatedProperties = [];
18+
19+
/** @var bool Whether accessors and mutators are enabled */
20+
protected $accessorsAndMutatorsEnabled = false;
21+
22+
/**
23+
* Get whether a property with the given key exists
24+
*
25+
* @param string $key
26+
*
27+
* @return bool
28+
*/
29+
public function hasProperty($key)
30+
{
31+
if (array_key_exists($key, $this->properties)) {
32+
return true;
33+
} elseif ($this->accessorsAndMutatorsEnabled) {
34+
$mutator = 'mutate' . Str::camel($key) . 'Property';
35+
36+
if (method_exists($this, $mutator)) {
37+
return true;
38+
}
39+
}
40+
41+
return false;
42+
}
43+
44+
/**
45+
* Set the given properties
46+
*
47+
* @param array $properties
48+
*
49+
* @return $this
50+
*/
51+
public function setProperties(array $properties)
52+
{
53+
foreach ($properties as $key => $value) {
54+
$this->setProperty($key, $value);
55+
}
56+
57+
return $this;
58+
}
59+
60+
/**
61+
* Get the property by the given key
62+
*
63+
* @param string $key
64+
*
65+
* @return mixed
66+
*
67+
* @throws OutOfBoundsException If the property by the given key does not exist
68+
*/
69+
protected function getProperty($key)
70+
{
71+
if ($this->accessorsAndMutatorsEnabled) {
72+
$this->mutateProperty($key);
73+
}
74+
75+
if (array_key_exists($key, $this->properties)) {
76+
if ($this->properties[$key] instanceof Closure) {
77+
$value = $this->properties[$key]($this);
78+
$this->setProperty($key, $value);
79+
return $value;
80+
}
81+
82+
return $this->properties[$key];
83+
}
84+
85+
throw new OutOfBoundsException("Can't access property '$key'. Property does not exist");
86+
}
87+
88+
/**
89+
* Set a property with the given key and value
90+
*
91+
* @param string $key
92+
* @param mixed $value
93+
*
94+
* @return $this
95+
*/
96+
protected function setProperty($key, $value)
97+
{
98+
$this->properties[$key] = $value;
99+
100+
if ($this->accessorsAndMutatorsEnabled) {
101+
$this->mutateProperty($key);
102+
}
103+
104+
return $this;
105+
}
106+
107+
/**
108+
* Try to mutate the given key
109+
*
110+
* @param string $key
111+
* @todo Support for generators, if needed
112+
*/
113+
protected function mutateProperty($key)
114+
{
115+
if (array_key_exists($key, $this->mutatedProperties)) {
116+
return;
117+
}
118+
119+
$value = array_key_exists($key, $this->properties)
120+
? $this->properties[$key]
121+
: null;
122+
$this->mutatedProperties[$key] = $value; // Prevents repeated checks
123+
124+
$mutator = Str::camel('mutate_' . $key) . 'Property';
125+
if (method_exists($this, $mutator)) {
126+
$this->properties[$key] = $this->$mutator($value);
127+
}
128+
}
129+
130+
/**
131+
* Check whether an offset exists
132+
*
133+
* @param mixed $offset
134+
*
135+
* @return bool
136+
*/
137+
public function offsetExists($offset)
138+
{
139+
if ($this->accessorsAndMutatorsEnabled) {
140+
$this->mutateProperty($offset);
141+
}
142+
143+
return isset($this->properties[$offset]);
144+
}
145+
146+
/**
147+
* Get the value for an offset
148+
*
149+
* @param mixed $offset
150+
*
151+
* @return mixed
152+
*/
153+
public function offsetGet($offset)
154+
{
155+
return $this->getProperty($offset);
156+
}
157+
158+
/**
159+
* Set the value for an offset
160+
*
161+
* @param mixed $offset
162+
* @param mixed $value
163+
*/
164+
public function offsetSet($offset, $value)
165+
{
166+
$this->setProperty($offset, $value);
167+
}
168+
169+
/**
170+
* Unset the value for an offset
171+
*
172+
* @param mixed $offset
173+
*/
174+
public function offsetUnset($offset)
175+
{
176+
unset($this->properties[$offset]);
177+
unset($this->mutatedProperties[$offset]);
178+
}
179+
180+
/**
181+
* Get the value of a non-public property
182+
*
183+
* This is a PHP magic method which is implicitly called upon access to non-public properties,
184+
* e.g. `$value = $object->property;`.
185+
* Do not call this method directly.
186+
*
187+
* @param mixed $key
188+
*
189+
* @return mixed
190+
*/
191+
public function __get($key)
192+
{
193+
return $this->getProperty($key);
194+
}
195+
196+
/**
197+
* Set the value of a non-public property
198+
*
199+
* This is a PHP magic method which is implicitly called upon access to non-public properties,
200+
* e.g. `$object->property = $value;`.
201+
* Do not call this method directly.
202+
*
203+
* @param string $key
204+
* @param mixed $value
205+
*/
206+
public function __set($key, $value)
207+
{
208+
$this->setProperty($key, $value);
209+
}
210+
211+
/**
212+
* Check whether a non-public property is defined and not null
213+
*
214+
* This is a PHP magic method which is implicitly called upon access to non-public properties,
215+
* e.g. `isset($object->property);`.
216+
* Do not call this method directly.
217+
*
218+
* @param string $key
219+
*
220+
* @return bool
221+
*/
222+
public function __isset($key)
223+
{
224+
return $this->offsetExists($key);
225+
}
226+
227+
/**
228+
* Unset the value of a non-public property
229+
*
230+
* This is a PHP magic method which is implicitly called upon access to non-public properties,
231+
* e.g. `unset($object->property);`. This method does nothing if the property does not exist.
232+
* Do not call this method directly.
233+
*
234+
* @param string $key
235+
*/
236+
public function __unset($key)
237+
{
238+
$this->offsetUnset($key);
239+
}
240+
}

tests/PropertiesTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace ipl\Tests\Stdlib;
4+
5+
use OutOfBoundsException;
6+
7+
class PropertiesTest extends \PHPUnit\Framework\TestCase
8+
{
9+
/**
10+
* @expectedException OutOfBoundsException
11+
*/
12+
public function testGetPropertyThrowsOutOfBoundsExceptionIfUnset()
13+
{
14+
$subject = new TestClassUsingThePropertiesTrait();
15+
16+
$subject->foo;
17+
}
18+
19+
/**
20+
* @expectedException OutOfBoundsException
21+
*/
22+
public function testArrayAccessThrowsOutOfBoundsExceptionIfUnset()
23+
{
24+
$subject = new TestClassUsingThePropertiesTrait();
25+
26+
$subject['foo'];
27+
}
28+
29+
public function testGetPropertyReturnsCorrectValueIfSet()
30+
{
31+
$subject = new TestClassUsingThePropertiesTrait();
32+
$subject->foo = 'bar';
33+
34+
$this->assertSame('bar', $subject->foo);
35+
}
36+
37+
public function testArrayAccessReturnsCorrectValueIfSet()
38+
{
39+
$subject = new TestClassUsingThePropertiesTrait();
40+
$subject['foo'] = 'bar';
41+
42+
$this->assertSame('bar', $subject['foo']);
43+
}
44+
45+
public function testIssetReturnsFalseForPropertyAccessIfUnset()
46+
{
47+
$subject = new TestClassUsingThePropertiesTrait();
48+
49+
$this->assertFalse(isset($subject->foo));
50+
}
51+
52+
public function testIssetReturnsFalseForArrayAccessIfUnset()
53+
{
54+
$subject = new TestClassUsingThePropertiesTrait();
55+
56+
$this->assertFalse(isset($subject['foo']));
57+
}
58+
59+
public function testIssetReturnsTrueForPropertyAccessIfSet()
60+
{
61+
$subject = new TestClassUsingThePropertiesTrait();
62+
$subject->foo = 'bar';
63+
64+
$this->assertTrue(isset($subject->foo));
65+
}
66+
67+
public function testIssetReturnsTrueForArrayAccessIfSet()
68+
{
69+
$subject = new TestClassUsingThePropertiesTrait();
70+
$subject->foo = 'bar';
71+
72+
$this->assertTrue(isset($subject['foo']));
73+
}
74+
75+
public function testUnsetForArrayAccess()
76+
{
77+
$subject = new TestClassUsingThePropertiesTrait();
78+
$subject['foo'] = 'bar';
79+
80+
$this->assertSame('bar', $subject['foo']);
81+
82+
unset($subject['foo']);
83+
84+
$this->expectException(OutOfBoundsException::class);
85+
$subject['foo'];
86+
}
87+
88+
public function testUnsetForPropertyAccess()
89+
{
90+
$subject = new TestClassUsingThePropertiesTrait();
91+
$subject->foo = 'bar';
92+
93+
$this->assertSame('bar', $subject->foo);
94+
95+
unset($subject->foo);
96+
97+
$this->expectException(OutOfBoundsException::class);
98+
$subject->foo;
99+
}
100+
101+
public function testGetMutatorGetsCalled()
102+
{
103+
$subject = new TestClassUsingThePropertiesTrait();
104+
105+
$this->assertSame('foobar', $subject->foobar);
106+
}
107+
108+
public function testSetMutatorGetsCalled()
109+
{
110+
$subject = new TestClassUsingThePropertiesTrait();
111+
$subject->special = 'foobar';
112+
113+
$this->assertSame('FOOBAR', $subject->special);
114+
}
115+
116+
public function testGetPropertiesReturnsEmptyArrayIfUnset()
117+
{
118+
$this->markTestSkipped('Properties::getProperties() not yet implemented');
119+
120+
$subject = new TestClassUsingThePropertiesTrait();
121+
122+
$this->assertSame([], $subject->getProperties());
123+
}
124+
125+
public function testGetPropertiesReturnsCorrectValueIfSet()
126+
{
127+
$this->markTestSkipped('Properties::getProperties() not yet implemented');
128+
129+
$subject = (new TestClassUsingThePropertiesTrait())
130+
->setProperties(['foo' => 'bar', 'baz' => 'qux']);
131+
132+
$this->assertSame(['foo' => 'bar', 'baz' => 'qux'], $subject->getProperties());
133+
}
134+
}

0 commit comments

Comments
 (0)