Skip to content

Commit 3aa5806

Browse files
authored
Merge pull request #6 from ingenerator/0.1/abstract-array-repo
Add AbstractArrayRepository
2 parents c5be055 + 63dee98 commit 3aa5806

File tree

3 files changed

+624
-0
lines changed

3 files changed

+624
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
### Unreleased
22

3+
### v0.1.6 (2018-09-06)
4+
5+
* Add AbstractArrayRepository
6+
37
### v0.1.5(2018-08-16)
48

59
* Add MysqlSession session handler
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
<?php
2+
/**
3+
* @author Andrew Coulton <andrew@ingenerator.com>
4+
* @licence BSD-3-Clause
5+
*/
6+
7+
namespace Ingenerator\PHPUtils\Repository;
8+
9+
use Doctrine\Common\Collections\Collection;
10+
use Ingenerator\PHPUtils\Object\ObjectPropertyPopulator;
11+
use PHPUnit\Framework\Assert;
12+
13+
/**
14+
* Base class for an array-based memory repository for use in unit tests. Provides helpers to allow
15+
* easy and quick creation of array-backed implementations of a project-specific repository interface.
16+
*
17+
* @package Ingenerator\PHPUtils\Repository
18+
*/
19+
abstract class AbstractArrayRepository
20+
{
21+
/**
22+
* @var array
23+
*/
24+
protected $entities;
25+
26+
/**
27+
* @var string
28+
*/
29+
protected $save_log;
30+
31+
protected function __construct(array $entities)
32+
{
33+
$this->entities = $entities;
34+
}
35+
36+
/**
37+
* Create a repo with the provided entities. Pass entities, or arrays of properties to stub
38+
*
39+
* @param array|object $entity,...
40+
*
41+
* @return static
42+
*/
43+
public static function with($entity)
44+
{
45+
return static::withList(func_get_args());
46+
}
47+
48+
/**
49+
* Same as ::with, but takes an array rather than a list of method parameters
50+
*
51+
* @param array[] $entity_data
52+
*
53+
* @return static
54+
*/
55+
public static function withList(array $entity_data)
56+
{
57+
$entity_class = static::getEntityBaseClass();
58+
$entities = [];
59+
foreach ($entity_data as $entity) {
60+
if ( ! $entity instanceof $entity_class) {
61+
$entity = static::stubEntity($entity);
62+
}
63+
$entities[] = $entity;
64+
}
65+
66+
return new static($entities);
67+
}
68+
69+
/**
70+
* @return string
71+
*/
72+
protected static function getEntityBaseClass()
73+
{
74+
throw new \BadMethodCallException('Implement your own '.__METHOD__.'!');
75+
}
76+
77+
/**
78+
* @param array $data
79+
*
80+
* @return object
81+
*/
82+
protected static function stubEntity(array $data)
83+
{
84+
$class = static::getEntityBaseClass();
85+
$e = new $class;
86+
ObjectPropertyPopulator::assignHash($e, $data);
87+
return $e;
88+
}
89+
90+
/**
91+
* @return static
92+
*/
93+
public static function withNothing()
94+
{
95+
return new static([]);
96+
}
97+
98+
/**
99+
* Ronseal
100+
*
101+
* (Does what it says on the tin)
102+
*/
103+
protected function assertNothingSaved()
104+
{
105+
Assert::assertEquals(
106+
'',
107+
$this->save_log,
108+
'Expected no saved entities'
109+
);
110+
}
111+
112+
/**
113+
* This, and only this, entity should have been saved
114+
*
115+
* @param object $entity
116+
*/
117+
protected function assertSavedOnly($entity)
118+
{
119+
Assert::assertEquals(
120+
$this->save_log,
121+
$this->formatSaveLog($entity),
122+
'Expected entity to be saved exactly once with matching data'
123+
);
124+
}
125+
126+
/**
127+
* Build a save-log record to allow the class to identify what's been saved for assertions
128+
*
129+
* @param object $entity
130+
*
131+
* @return string
132+
*/
133+
protected function formatSaveLog($entity)
134+
{
135+
return sprintf(
136+
"%s (object %s) with data:\n%s\n",
137+
get_class($entity),
138+
spl_object_hash($entity),
139+
json_encode($this->entityToArray($entity), JSON_PRETTY_PRINT)
140+
);
141+
}
142+
143+
/**
144+
* Creates a simple array representation of a set of entities that can be formatted as JSON
145+
*
146+
* Used to capture a snapshot of entity state at the time it's saved to allow later comparison
147+
*
148+
* @param object $entity
149+
* @param array $seen_objects
150+
*
151+
* @return array
152+
*/
153+
protected function entityToArray($entity, & $seen_objects = [])
154+
{
155+
$entity_hash = spl_object_hash($entity);
156+
if (isset($seen_objects[$entity_hash])) {
157+
return '**RECURSION**';
158+
} else {
159+
$seen_objects[$entity_hash] = TRUE;
160+
}
161+
162+
$all_props = \Closure::bind(
163+
function ($e) {
164+
return get_object_vars($e);
165+
},
166+
NULL,
167+
$entity
168+
);
169+
$obj_identity = function ($a) {
170+
return get_class($a).'#'.spl_object_hash($a);
171+
};
172+
$result = [];
173+
foreach ($all_props($entity) as $key => $var) {
174+
if ( ! is_object($var)) {
175+
$result[$key] = $var;
176+
} elseif ($var instanceof Collection) {
177+
$result[$key] = [];
178+
foreach ($var as $collection_item) {
179+
$result[$key][] = [
180+
$obj_identity($var) => $this->entityToArray($collection_item, $seen_objects)
181+
];
182+
}
183+
} elseif ($var instanceof \DateTimeInterface) {
184+
$result[$key][get_class($var)] = $var->format(\DateTime::ISO8601);
185+
} else {
186+
$result[$key] = [
187+
$obj_identity($var) => $this->entityToArray($var, $seen_objects)
188+
];
189+
}
190+
}
191+
192+
return $result;
193+
}
194+
195+
/**
196+
* Count entities by a group value returned by the callback
197+
*
198+
* public function countByColour() {
199+
* return $this->countWith(
200+
* function (MyEntity $e) { return $e->getColour(); }
201+
* );
202+
* }
203+
*
204+
* @param callable $callable
205+
*
206+
* @return int[]
207+
*/
208+
protected function countWith($callable)
209+
{
210+
$counts = [];
211+
foreach ($this->entities as $entity) {
212+
$group = call_user_func($callable, $entity);
213+
$counts[$group] = isset($counts[$group]) ? ++$counts[$group] : 1;
214+
}
215+
return $counts;
216+
}
217+
218+
/**
219+
* Find a single entity matching the callback (throws if non-unique or nothing matching)
220+
*
221+
* public function load($id) {
222+
* return $this->loadWith(function (MyEntity $e) use ($id) { return $e->getId() === $id; });
223+
* }
224+
*
225+
* @param callable $callable
226+
*
227+
* @return object
228+
*/
229+
protected function loadWith($callable)
230+
{
231+
if ( ! $entity = $this->findWith($callable)) {
232+
throw new \InvalidArgumentException('No entity matching criteria');
233+
}
234+
235+
return $entity;
236+
}
237+
238+
/**
239+
* Find a single entity matching the callback, or null (throws if non-unique)
240+
*
241+
* @param $callable
242+
*
243+
* @return object
244+
*/
245+
protected function findWith($callable)
246+
{
247+
$entities = $this->listWith($callable);
248+
if (count($entities) > 1) {
249+
throw new \UnexpectedValueException(
250+
'Found multiple entities : expected unique condition.'
251+
);
252+
}
253+
254+
return array_pop($entities);
255+
}
256+
257+
/**
258+
* Find all entities that the callable matches (like array_filter)
259+
*
260+
* @param callable $callable
261+
*
262+
* @return object[]
263+
*/
264+
protected function listWith($callable)
265+
{
266+
$entities = [];
267+
foreach ($this->entities as $entity) {
268+
if (call_user_func($callable, $entity)) {
269+
$entities[] = $entity;
270+
}
271+
}
272+
273+
return $entities;
274+
}
275+
276+
/**
277+
* Use this to implement repository methods that save entities.
278+
*
279+
* The state of all entity properties will be captured at time of save to allow verifying that
280+
* it hasn't been subsequently modified.
281+
*
282+
* @param object $entity
283+
*/
284+
protected function saveEntity($entity)
285+
{
286+
$this->save_log .= $this->formatSaveLog($entity);
287+
if ( ! in_array($entity, $this->entities, TRUE)) {
288+
$this->entities[] = $entity;
289+
}
290+
}
291+
292+
}

0 commit comments

Comments
 (0)