Complex classes like Mapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Mapper, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
23 | class Mapper |
||
24 | { |
||
25 | /** |
||
26 | * The Manager instance |
||
27 | * |
||
28 | * @var \Analogue\ORM\System\Manager |
||
29 | */ |
||
30 | protected $manager; |
||
31 | |||
32 | /** |
||
33 | * Instance of EntityMapper Object |
||
34 | * |
||
35 | * @var \Analogue\ORM\EntityMap |
||
36 | */ |
||
37 | protected $entityMap; |
||
38 | |||
39 | /** |
||
40 | * The instance of db adapter |
||
41 | * |
||
42 | * @var \Analogue\ORM\Drivers\DBAdapter |
||
43 | */ |
||
44 | protected $adapter; |
||
45 | |||
46 | |||
47 | /** |
||
48 | * Event dispatcher instance |
||
49 | * |
||
50 | * @var \Illuminate\Contracts\Events\Dispatcher |
||
51 | */ |
||
52 | protected $dispatcher; |
||
53 | |||
54 | /** |
||
55 | * Entity Cache |
||
56 | * |
||
57 | * @var \Analogue\ORM\System\EntityCache |
||
58 | */ |
||
59 | protected $cache; |
||
60 | |||
61 | /** |
||
62 | * Global scopes |
||
63 | * |
||
64 | * @var array |
||
65 | */ |
||
66 | protected $globalScopes = []; |
||
67 | |||
68 | /** |
||
69 | * Custom Commands |
||
70 | * |
||
71 | * @var array |
||
72 | */ |
||
73 | protected $customCommands = []; |
||
74 | |||
75 | /** |
||
76 | * @param EntityMap $entityMap |
||
77 | * @param DBAdapter $adapter |
||
78 | * @param Dispatcher $dispatcher |
||
79 | * @param Manager $manager |
||
80 | */ |
||
81 | public function __construct(EntityMap $entityMap, DBAdapter $adapter, Dispatcher $dispatcher, Manager $manager) |
||
93 | |||
94 | /** |
||
95 | * Persist an entity or an entity collection into the database |
||
96 | * |
||
97 | * @param Mappable|\Traversable|array $entity |
||
98 | * @throws \InvalidArgumentException |
||
99 | * @throws MappingException |
||
100 | * @return Mappable|\Traversable|array |
||
101 | */ |
||
102 | public function store($entity) |
||
110 | |||
111 | /** |
||
112 | * Store an entity collection inside a single DB Transaction |
||
113 | * |
||
114 | * @param \Traversable|array $entities |
||
115 | * @throws \InvalidArgumentException |
||
116 | * @throws MappingException |
||
117 | * @return \Traversable|array |
||
118 | */ |
||
119 | protected function storeCollection($entities) |
||
120 | { |
||
121 | $this->adapter->beginTransaction(); |
||
122 | |||
123 | foreach ($entities as $entity) { |
||
124 | $this->storeEntity($entity); |
||
125 | } |
||
126 | |||
127 | $this->adapter->commit(); |
||
128 | |||
129 | return $entities; |
||
130 | } |
||
131 | |||
132 | /** |
||
133 | * Store a single entity into the database |
||
134 | * |
||
135 | * @param Mappable $entity |
||
136 | * @throws \InvalidArgumentException |
||
137 | * @throws MappingException |
||
138 | * @return \Analogue\ORM\Entity |
||
139 | */ |
||
140 | protected function storeEntity($entity) |
||
148 | |||
149 | /** |
||
150 | * Check that the entity correspond to the current mapper. |
||
151 | * |
||
152 | * @param mixed $entity |
||
153 | * @throws InvalidArgumentException |
||
154 | * @return void |
||
155 | */ |
||
156 | protected function checkEntityType($entity) |
||
157 | { |
||
158 | if (get_class($entity) != $this->entityMap->getClass()) { |
||
159 | $expected = $this->entityMap->getClass(); |
||
160 | $actual = get_class($entity); |
||
161 | throw new InvalidArgumentException("Expected : $expected, got $actual."); |
||
162 | } |
||
163 | } |
||
164 | |||
165 | /** |
||
166 | * Convert an entity into an aggregate root |
||
167 | * |
||
168 | * @param mixed $entity |
||
169 | * @throws MappingException |
||
170 | * @return \Analogue\ORM\System\Aggregate |
||
171 | */ |
||
172 | protected function aggregate($entity) |
||
173 | { |
||
174 | return new Aggregate($entity); |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * Get a the Underlying QueryAdapter. |
||
179 | * |
||
180 | * @return \Analogue\ORM\Drivers\QueryAdapter |
||
181 | */ |
||
182 | public function newQueryBuilder() |
||
183 | { |
||
184 | return $this->adapter->getQuery(); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Delete an entity or an entity collection from the database |
||
189 | * |
||
190 | * @param Mappable|\Traversable|array |
||
191 | * @throws MappingException |
||
192 | * @throws \InvalidArgumentException |
||
193 | * @return \Traversable|array |
||
194 | */ |
||
195 | public function delete($entity) |
||
196 | { |
||
197 | if ($this->manager->isTraversable($entity)) { |
||
198 | return $this->deleteCollection($entity); |
||
199 | } else { |
||
200 | $this->deleteEntity($entity); |
||
201 | } |
||
202 | } |
||
203 | |||
204 | /** |
||
205 | * Delete an Entity Collection inside a single db transaction |
||
206 | * |
||
207 | * @param \Traversable|array $entities |
||
208 | * @throws \InvalidArgumentException |
||
209 | * @throws MappingException |
||
210 | * @return \Traversable|array |
||
211 | */ |
||
212 | protected function deleteCollection($entities) |
||
213 | { |
||
214 | $this->adapter->beginTransaction(); |
||
215 | |||
216 | foreach ($entities as $entity) { |
||
217 | $this->deleteEntity($entity); |
||
218 | } |
||
219 | |||
220 | $this->adapter->commit(); |
||
221 | |||
222 | return $entities; |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * Delete a single entity from the database. |
||
227 | * |
||
228 | * @param Mappable $entity |
||
229 | * @throws \InvalidArgumentException |
||
230 | * @throws MappingException |
||
231 | * @return void |
||
232 | */ |
||
233 | protected function deleteEntity($entity) |
||
234 | { |
||
235 | $this->checkEntityType($entity); |
||
236 | |||
237 | $delete = new Delete($this->aggregate($entity), $this->newQueryBuilder()); |
||
238 | |||
239 | $delete->execute(); |
||
240 | } |
||
241 | |||
242 | /** |
||
243 | * Return the entity map for this mapper |
||
244 | * |
||
245 | * @return EntityMap |
||
246 | */ |
||
247 | public function getEntityMap() |
||
248 | { |
||
249 | return $this->entityMap; |
||
250 | } |
||
251 | |||
252 | /** |
||
253 | * Get the entity cache for the current mapper |
||
254 | * |
||
255 | * @return EntityCache $entityCache |
||
256 | */ |
||
257 | public function getEntityCache() |
||
261 | |||
262 | /** |
||
263 | * Fire the given event for the entity |
||
264 | * |
||
265 | * @param string $event |
||
266 | * @param \Analogue\ORM\Entity $entity |
||
267 | * @param bool $halt |
||
268 | * @throws InvalidArgumentException |
||
269 | * @return mixed |
||
270 | */ |
||
271 | public function fireEvent($event, $entity, $halt = true) |
||
272 | { |
||
273 | if ($entity instanceof Wrapper) { |
||
274 | throw new InvalidArgumentException('Fired Event with invalid Entity Object'); |
||
275 | } |
||
276 | |||
277 | $event = "analogue.{$event}." . $this->entityMap->getClass(); |
||
278 | |||
279 | $method = $halt ? 'until' : 'fire'; |
||
280 | |||
281 | return $this->dispatcher->$method($event, $entity); |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * Register an entity event with the dispatcher. |
||
286 | * |
||
287 | * @param string $event |
||
288 | * @param \Closure $callback |
||
289 | * @return void |
||
290 | */ |
||
291 | public function registerEvent($event, $callback) |
||
297 | |||
298 | /** |
||
299 | * Add a global scope to this mapper query builder |
||
300 | * |
||
301 | * @param ScopeInterface $scope |
||
302 | * @return void |
||
303 | */ |
||
304 | public function addGlobalScope(ScopeInterface $scope) |
||
308 | |||
309 | /** |
||
310 | * Determine if the mapper has a global scope. |
||
311 | * |
||
312 | * @param \Analogue\ORM\System\ScopeInterface $scope |
||
313 | * @return bool |
||
314 | */ |
||
315 | public function hasGlobalScope($scope) |
||
319 | |||
320 | /** |
||
321 | * Get a global scope registered with the modal. |
||
322 | * |
||
323 | * @param \Analogue\ORM\System\ScopeInterface $scope |
||
324 | * @return \Analogue\ORM\System\ScopeInterface|null |
||
325 | */ |
||
326 | public function getGlobalScope($scope) |
||
327 | { |
||
328 | return array_first($this->globalScopes, function ($key, $value) use ($scope) { |
||
329 | return $scope instanceof $value; |
||
330 | }); |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * Get a new query instance without a given scope. |
||
335 | * |
||
336 | * @param \Analogue\ORM\System\ScopeInterface $scope |
||
337 | * @return \Analogue\ORM\System\Query |
||
338 | */ |
||
339 | public function newQueryWithoutScope($scope) |
||
340 | { |
||
341 | $this->getGlobalScope($scope)->remove($query = $this->getQuery(), $this); |
||
342 | |||
343 | return $query; |
||
344 | } |
||
345 | |||
346 | /** |
||
347 | * Get the Analogue Query Builder for this instance |
||
348 | * |
||
349 | * @return \Analogue\ORM\System\Query |
||
350 | */ |
||
351 | public function getQuery() |
||
352 | { |
||
353 | $query = new Query($this, $this->adapter); |
||
354 | |||
355 | return $this->applyGlobalScopes($query); |
||
356 | } |
||
357 | |||
358 | /** |
||
359 | * Apply all of the global scopes to an Analogue Query builder. |
||
360 | * |
||
361 | * @param Query $query |
||
362 | * @return \Analogue\ORM\System\Query |
||
363 | */ |
||
364 | public function applyGlobalScopes($query) |
||
365 | { |
||
366 | foreach ($this->getGlobalScopes() as $scope) { |
||
367 | $scope->apply($query, $this); |
||
368 | } |
||
369 | |||
370 | return $query; |
||
371 | } |
||
372 | |||
373 | /** |
||
374 | * Get the global scopes for this class instance. |
||
375 | * |
||
376 | * @return \Analogue\ORM\System\ScopeInterface |
||
377 | */ |
||
378 | public function getGlobalScopes() |
||
379 | { |
||
380 | return $this->globalScopes; |
||
381 | } |
||
382 | |||
383 | /** |
||
384 | * Add a dynamic method that extends the mapper/repository |
||
385 | * |
||
386 | * @param string $command |
||
387 | */ |
||
388 | public function addCustomCommand($command) |
||
394 | |||
395 | /** |
||
396 | * Create a new instance of the mapped entity class |
||
397 | * |
||
398 | * @param array $attributes |
||
399 | * @return mixed |
||
400 | */ |
||
401 | public function newInstance($attributes = []) |
||
402 | { |
||
403 | $class = $this->entityMap->getClass(); |
||
404 | |||
405 | if ($this->entityMap->activator() != null) { |
||
406 | $entity = $this->entityMap->activator(); |
||
407 | } else { |
||
408 | $entity = $this->customClassInstance($class); |
||
409 | } |
||
410 | |||
411 | // prevent hydrating with an empty array |
||
412 | if (count($attributes) > 0) { |
||
413 | $entity->setEntityAttributes($attributes); |
||
414 | } |
||
415 | |||
416 | return $entity; |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * Use a trick to generate a class prototype that we |
||
421 | * can instantiate without calling the constructor. |
||
422 | * |
||
423 | * @param string|null $className |
||
424 | * @throws MappingException |
||
425 | * @return mixed |
||
426 | */ |
||
427 | protected function customClassInstance($className) |
||
428 | { |
||
429 | if (!class_exists($className)) { |
||
430 | throw new MappingException("Tried to instantiate a non-existing Entity class : $className"); |
||
431 | } |
||
432 | |||
433 | $prototype = unserialize(sprintf('O:%d:"%s":0:{}', strlen($className), $className)); |
||
434 | |||
435 | return $prototype; |
||
436 | } |
||
437 | |||
438 | /** |
||
439 | * Get an unscoped Analogue Query Builder for this instance |
||
440 | * |
||
441 | * @return \Analogue\ORM\System\Query |
||
442 | */ |
||
443 | public function globalQuery() |
||
444 | { |
||
445 | return $this->newQueryWithoutScopes(); |
||
446 | } |
||
447 | |||
448 | /** |
||
449 | * Get a new query builder that doesn't have any global scopes. |
||
450 | * |
||
451 | * @return Query |
||
452 | */ |
||
453 | public function newQueryWithoutScopes() |
||
454 | { |
||
455 | return $this->removeGlobalScopes($this->getQuery()); |
||
456 | } |
||
457 | |||
458 | /** |
||
459 | * Remove all of the global scopes from an Analogue Query builder. |
||
460 | * |
||
461 | * @param Query $query |
||
462 | * @return \Analogue\ORM\System\Query |
||
463 | */ |
||
464 | public function removeGlobalScopes($query) |
||
465 | { |
||
466 | foreach ($this->getGlobalScopes() as $scope) { |
||
467 | $scope->remove($query, $this); |
||
468 | } |
||
469 | |||
470 | return $query; |
||
471 | } |
||
472 | |||
473 | /** |
||
474 | * Return the manager instance |
||
475 | * |
||
476 | * @return \Analogue\ORM\System\Manager |
||
477 | */ |
||
478 | public function getManager() |
||
482 | |||
483 | /** |
||
484 | * Dynamically handle calls to custom commands, or Redirects to query() |
||
485 | * |
||
486 | * @param string $method |
||
487 | * @param array $parameters |
||
488 | * @throws \Exception |
||
489 | * @return mixed |
||
490 | */ |
||
491 | public function __call($method, $parameters) |
||
492 | { |
||
493 | // Check if method is a custom command on the mapper |
||
494 | if ($this->hasCustomCommand($method)) { |
||
495 | if (count($parameters) == 0) { |
||
496 | throw new \Exception("$method must at least have 1 argument"); |
||
497 | } |
||
498 | |||
499 | return $this->executeCustomCommand($method, $parameters[0]); |
||
500 | } |
||
501 | |||
505 | |||
506 | /** |
||
507 | * Check if this mapper supports this command |
||
508 | * @param string $command |
||
509 | * @return boolean |
||
510 | */ |
||
511 | public function hasCustomCommand($command) |
||
515 | |||
516 | /** |
||
517 | * Get all the custom commands registered on this mapper |
||
518 | * |
||
519 | * @return array |
||
520 | */ |
||
521 | public function getCustomCommands() |
||
525 | |||
526 | /** |
||
527 | * Execute a custom command on an Entity |
||
528 | * |
||
529 | * @param string $command |
||
530 | * @param mixed|Collection|array $entity |
||
531 | * @throws \InvalidArgumentException |
||
532 | * @throws MappingException |
||
533 | * @return mixed |
||
534 | */ |
||
535 | public function executeCustomCommand($command, $entity) |
||
547 | |||
548 | /** |
||
549 | * Execute a single command instance |
||
550 | * |
||
551 | * @param string $commandClass |
||
552 | * @param mixed $entity |
||
553 | * @throws \InvalidArgumentException |
||
554 | * @throws MappingException |
||
555 | * @return mixed |
||
556 | */ |
||
557 | protected function executeSingleCustomCommand($commandClass, $entity) |
||
565 | |||
566 | /** |
||
567 | * Get the Analogue Query Builder for this instance |
||
568 | * |
||
569 | * @return \Analogue\ORM\System\Query |
||
570 | */ |
||
571 | public function query() |
||
575 | } |
||
576 |
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.