Skip to content

Commit 33b29e0

Browse files
committed
chore: Initial idea for a lightweight data mapper
1 parent 819d490 commit 33b29e0

File tree

10 files changed

+695
-0
lines changed

10 files changed

+695
-0
lines changed

src/Concerns/UsesTransactions.php

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Articulate\Concise\Concerns;
5+
6+
use Closure;
7+
8+
/**
9+
* @phpstan-require-extends \Articulate\Concise\Support\BaseRepository
10+
*
11+
* @mixin \Articulate\Concise\Support\BaseRepository
12+
*/
13+
trait UsesTransactions
14+
{
15+
/**
16+
* Executes a given callback within a database transaction.
17+
*
18+
* @template RetType of mixed
19+
*
20+
* @param Closure(\Illuminate\Database\Connection): RetType $callback The callback function to execute within the transaction.
21+
*
22+
* @return mixed The result of the callback function execution.
23+
*
24+
* @throws \Throwable
25+
*/
26+
protected function inTransaction(Closure $callback): mixed
27+
{
28+
return $this->connection()->transaction($callback);
29+
}
30+
31+
/**
32+
* Initiates a database transaction.
33+
*
34+
* @return void
35+
*
36+
* @throws \Throwable
37+
*/
38+
protected function beginTransaction(): void
39+
{
40+
$this->connection()->beginTransaction();
41+
}
42+
43+
/**
44+
* Finalizes a database transaction, committing all changes made during the transaction.
45+
*
46+
* @return void
47+
*
48+
* @throws \Throwable
49+
*/
50+
protected function commitTransaction(): void
51+
{
52+
$this->connection()->commit();
53+
}
54+
55+
/**
56+
* Rolls back the current database transaction, reverting any changes made
57+
* during the transaction.
58+
*
59+
* @return void
60+
* @throws \Throwable
61+
*/
62+
protected function rollBackTransaction(): void
63+
{
64+
$this->connection()->rollBack();
65+
}
66+
}

src/Concise.php

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Articulate\Concise;
5+
6+
use Articulate\Concise\Contracts\EntityMapper;
7+
use Articulate\Concise\Contracts\Repository;
8+
use Closure;
9+
use Illuminate\Contracts\Foundation\Application;
10+
use Illuminate\Database\DatabaseManager;
11+
use InvalidArgumentException;
12+
13+
final class Concise
14+
{
15+
/**
16+
* The Laravel application
17+
*
18+
* @var \Illuminate\Contracts\Foundation\Application
19+
*/
20+
private Application $app;
21+
22+
/**
23+
* @var \Articulate\Concise\IdentityMap
24+
*/
25+
private IdentityMap $identities;
26+
27+
/**
28+
* Entity mappers mapped by the entity class.
29+
*
30+
* @var array<class-string, \Articulate\Concise\Contracts\EntityMapper<*>>
31+
*/
32+
private array $entityMappers = [];
33+
34+
/**
35+
* Component mappers mapped by the component class.
36+
*
37+
* @var array<class-string, \Articulate\Concise\Contracts\Mapper<*>>
38+
*/
39+
private array $componentMappers = [];
40+
41+
/**
42+
* @var array<class-string, \Articulate\Concise\Contracts\Repository<*>>
43+
*/
44+
private array $repositories = [];
45+
46+
public function __construct(Application $app)
47+
{
48+
$this->app = $app;
49+
$this->identities = new IdentityMap();
50+
}
51+
52+
/**
53+
* Get all entity mappers
54+
*
55+
* @return array<class-string, \Articulate\Concise\Contracts\EntityMapper<*>>
56+
*/
57+
public function getEntityMappers(): array
58+
{
59+
return $this->entityMappers;
60+
}
61+
62+
/**
63+
* Registers the provided Mapper instance.
64+
*
65+
* @template ObjType of object
66+
*
67+
* @param class-string<\Articulate\Concise\Contracts\Mapper<ObjType>> $mapperClass The mapper class to register.
68+
*
69+
* @return self Returns the current instance for method chaining.
70+
*
71+
* @throws \Illuminate\Contracts\Container\BindingResolutionException
72+
*/
73+
public function register(string $mapperClass): self
74+
{
75+
$mapper = $this->app->make($mapperClass);
76+
77+
if ($mapper instanceof EntityMapper) {
78+
$this->entityMappers[$mapper->getClass()] = $mapper;
79+
} else {
80+
$this->componentMappers[$mapper->getClass()] = $mapper;
81+
}
82+
83+
return $this;
84+
}
85+
86+
/**
87+
* Creates an entity of the given class for the give data
88+
*
89+
* @template EntityType of object
90+
*
91+
* @param class-string<EntityType> $class The fully qualified class name of the entity.
92+
* @param array<string, mixed> $data The data to create the entity from.
93+
*
94+
* @return object The created entity
95+
*
96+
* @phpstan-return EntityType
97+
*
98+
* @throws InvalidArgumentException If no entity mapper is registered for the provided class.
99+
*/
100+
public function entity(string $class, array $data): object
101+
{
102+
/** @var \Articulate\Concise\Contracts\EntityMapper<EntityType>|null $mapper */
103+
$mapper = $this->entityMappers[$class] ?? null;
104+
105+
if ($mapper === null) {
106+
throw new InvalidArgumentException("No entity mapper registered for {$class}.");
107+
}
108+
109+
return $this->identified($mapper, $data);
110+
}
111+
112+
/**
113+
* Resolve and retrieve an identified entity
114+
*
115+
* @template EntityType of object
116+
*
117+
* @param \Articulate\Concise\Contracts\EntityMapper<EntityType> $mapper
118+
* @param array<string, mixed> $data
119+
*
120+
* @return object
121+
*
122+
* @phpstan-return EntityType
123+
*/
124+
public function identified(EntityMapper $mapper, array $data): object
125+
{
126+
// Get the identity from the data
127+
$identity = $mapper->identity($data);
128+
129+
// Retrieve an existing entity if one does exist
130+
$existing = $this->identities->get($mapper->getClass(), $identity);
131+
132+
// If it does exist, return it instead
133+
if ($existing !== null) {
134+
return $existing;
135+
}
136+
137+
// If it doesn't exist, create a new entity from the data
138+
$entity = $mapper->toObject($data);
139+
140+
// And then map its identity
141+
$this->identities->add($entity, $identity, $mapper->getClass());
142+
143+
return $entity;
144+
}
145+
146+
/**
147+
* Create a component of the given class for the given data.
148+
*
149+
* @template ComponentType of object
150+
*
151+
* @param class-string<ComponentType> $class The fully qualified class name of the component.
152+
* @param array<string, mixed> $data The data to create the component from.
153+
*
154+
* @return object The created component
155+
*
156+
* @phpstan-return ComponentType
157+
*
158+
* @throws InvalidArgumentException If no component mapper is registered for the provided class.
159+
*/
160+
public function component(string $class, array $data): object
161+
{
162+
/** @var \Articulate\Concise\Contracts\Mapper<ComponentType>|null $mapper */
163+
$mapper = $this->componentMappers[$class] ?? null;
164+
165+
if ($mapper === null) {
166+
throw new InvalidArgumentException("No component mapper registered for {$class}.");
167+
}
168+
169+
// Components don't have "identities", so we can return this
170+
171+
return $mapper->toObject($data);
172+
}
173+
174+
/**
175+
* Get the data representation for the given object.
176+
*
177+
* @template ObjType of object
178+
*
179+
* @param object $object The object to convert into a data array
180+
*
181+
* @phpstan-param ObjType $object
182+
*
183+
* @return array<string, mixed> The data
184+
*/
185+
public function data(object $object): array
186+
{
187+
/** @var \Articulate\Concise\Contracts\Mapper<ObjType>|null $mapper */
188+
$mapper = $this->entityMappers[$object::class] ?? $this->componentMappers[$object::class] ?? null;
189+
190+
if ($mapper === null) {
191+
throw new InvalidArgumentException('No mapper registered for ' . $object::class . '.');
192+
}
193+
194+
return $mapper->toData($object);
195+
}
196+
197+
/**
198+
* Get a repository instance for the given entity class.
199+
*
200+
* @template EntityType of object
201+
*
202+
* @param class-string<EntityType> $class
203+
*
204+
* @return \Articulate\Concise\Contracts\Repository<EntityType>
205+
*
206+
* @throws \Illuminate\Contracts\Container\BindingResolutionException
207+
*/
208+
public function repository(string $class): Repository
209+
{
210+
$mapper = $this->entityMappers[$class] ?? null;
211+
212+
if ($mapper === null) {
213+
throw new InvalidArgumentException('No mapper registered for ' . $class . '.');
214+
}
215+
216+
$repository = $this->repositories[$class] ?? null;
217+
218+
if ($repository !== null) {
219+
return $repository;
220+
/** @phpstan-ignore return.type */
221+
}
222+
223+
$repositoryClass = $mapper->repository();
224+
225+
return $this->repositories[$class] = new $repositoryClass(
226+
$this,
227+
$mapper,
228+
$this->app->make(DatabaseManager::class)->connection($mapper->connection())
229+
);
230+
}
231+
}

src/ConciseServiceProvider.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,25 @@
77

88
class ConciseServiceProvider extends ServiceProvider
99
{
10+
public function register(): void
11+
{
12+
// Register the Concise class
13+
$this->app->singleton(Concise::class, function () {
14+
return new Concise($this->app);
15+
});
1016

17+
// Register the mappers' repositories
18+
$this->booted($this->registerRepositories(...));
19+
}
20+
21+
protected function registerRepositories(Concise $concise): void
22+
{
23+
foreach ($concise->getEntityMappers() as $mapper) {
24+
$repo = $mapper->repository();
25+
26+
if ($repo !== null) {
27+
$this->app->bind($repo, fn () => $concise->repository($mapper->getClass()));
28+
}
29+
}
30+
}
1131
}

src/Contracts/EntityMapper.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace Articulate\Concise\Contracts;
4+
5+
/**
6+
* EntityMapper
7+
*
8+
* Defines an interface for mapping entities.
9+
*
10+
* @template EntityType of object
11+
*
12+
* @extends \Articulate\Concise\Contracts\Mapper<EntityType>
13+
*/
14+
interface EntityMapper extends Mapper
15+
{
16+
/**
17+
* Get the connection name the entity uses.
18+
*
19+
* @return string|null The connection name or null to use the default.
20+
*/
21+
public function connection(): ?string;
22+
23+
/**
24+
* Get the repository class name.
25+
*
26+
* @return class-string<\Articulate\Concise\Contracts\Repository<EntityType>>|null
27+
*/
28+
public function repository(): ?string;
29+
30+
/**
31+
* Get the identity from the provided data.
32+
*
33+
* @param array<string, mixed>|object $data
34+
*
35+
* @phpstan-param array<string, mixed>|EntityType $data
36+
*
37+
* @return int|string
38+
*/
39+
public function identity(array|object $data): int|string;
40+
}

0 commit comments

Comments
 (0)