Skip to content

Commit 23f5a6a

Browse files
Merge pull request #1 from JsonMapper/feature/Add-middleware
Add initial middleware
2 parents 923a159 + 697cc14 commit 23f5a6a

File tree

9 files changed

+442
-14
lines changed

9 files changed

+442
-14
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ matrix:
2828
- php: nightly
2929
fast_finish: true
3030

31+
branches:
32+
only:
33+
- master
34+
- develop
35+
3136
install:
3237
- curl -s http://getcomposer.org/installer | php
3338
- php composer.phar install --dev --no-interaction $COMPOSER_FLAGS

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
### Added
9+
- Allow Eloquent models to be populated from Json.

README.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
JsonMapper is a PHP library that allows you to map a JSON response to your PHP objects that are either annotated using doc blocks or use typed properties.
77
For more information see the project website: https://jsonmapper.net
88

9-
![GitHub](https://img.shields.io/github/license/JsonMapper/LaravelPackage)
10-
![Packagist Version](https://img.shields.io/packagist/v/json-mapper/laravel-package)
11-
![PHP from Packagist](https://img.shields.io/packagist/php-v/json-mapper/laravel-package)
12-
[![Build Status](https://api.travis-ci.com/JsonMapper/LaravelPackage.svg?branch=master)](https://travis-ci.com/JsonMapper/LaravelPackage)
13-
[![Coverage Status](https://coveralls.io/repos/github/JsonMapper/LaravelPackage/badge.svg?branch=master)](https://coveralls.io/github/JsonMapper/LaravelPackage?branch=master)
9+
![GitHub](https://img.shields.io/github/license/JsonMapper/EloquentMiddleware)
10+
![Packagist Version](https://img.shields.io/packagist/v/json-mapper/eloquent-middleware)
11+
![PHP from Packagist](https://img.shields.io/packagist/php-v/json-mapper/eloquent-middleware)
12+
[![Build Status](https://api.travis-ci.com/JsonMapper/EloquentMiddleware.svg?branch=master)](https://travis-ci.com/JsonMapper/EloquentMiddleware)
13+
[![Coverage Status](https://coveralls.io/repos/github/JsonMapper/EloquentMiddleware/badge.svg?branch=master)](https://coveralls.io/github/JsonMapper/EloquentMiddleware?branch=master)
1414

1515
# Why use JsonMapper
1616
Continuously mapping your JSON responses to your own objects becomes tedious and is error prone. Not mentioning the
@@ -27,18 +27,18 @@ JsonMapper supports the following features
2727
* Namespace resolving
2828
* PHP 7.4 Types properties
2929

30-
# Installing JsonMapper laravel package
31-
The installation of JsonMapper Laravel package can easily be done with [Composer](https://getcomposer.org)
30+
# Installing JsonMapper Eloquent Middleware
31+
The installation of JsonMapper Eloquent Middleware can easily be done with [Composer](https://getcomposer.org)
3232
```bash
33-
$ composer require json-mapper/laravel-package
33+
$ composer require json-mapper/eloquent-middleware
3434
```
3535
The example shown above assumes that `composer` is on your `$PATH`.
3636

3737
# Contributing
38-
Please refer to [CONTRIBUTING.md](https://github.com/JsonMapper/LaravelPackage/blob/master/CONTRIBUTING.md) for information on how to contribute to JsonMapper Laravel package.
38+
Please refer to [CONTRIBUTING.md](https://github.com/JsonMapper/EloquentMiddleware/blob/master/CONTRIBUTING.md) for information on how to contribute to JsonMapper Eloquent Middleware.
3939

4040
## List of Contributors
41-
Thanks to everyone who has contributed to JsonMapper Laravel package! You can find a detailed list of contributors of JsonMapper on [GitHub](https://github.com/JsonMapper/LaravelPackage/graphs/contributors).
41+
Thanks to everyone who has contributed to JsonMapper Eloquent Middleware! You can find a detailed list of contributors of JsonMapper on [GitHub](https://github.com/JsonMapper/EloquentMiddleware/graphs/contributors).
4242

4343
# License
44-
The MIT License (MIT). Please see [License File](https://github.com/JsonMapper/LaravelPackage/blob/master/LICENSE) for more information.
44+
The MIT License (MIT). Please see [License File](https://github.com/JsonMapper/EloquentMiddleware/blob/master/LICENSE) for more information.

composer.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,15 @@
2525
"require": {
2626
"json-mapper/laravel-package": "^1.1",
2727
"php": "^7.2 || ^8.0",
28-
"doctrine/dbal": "^2.0"
28+
"doctrine/dbal": "^2.3",
29+
"json-mapper/json-mapper": "^1.1"
2930
},
3031
"require-dev": {
3132
"squizlabs/php_codesniffer": "^3.5",
3233
"phpstan/phpstan": "^0.12.19",
3334
"php-coveralls/php-coveralls": "^2.2",
34-
"phpunit/phpunit": "^8.0",
35-
"orchestra/testbench": "^5.3"
35+
"orchestra/testbench": "^5.3",
36+
"phpunit/phpunit": "^8.0|^9.0"
3637
},
3738
"scripts": {
3839
"phpcs": "phpcs --standard=PSR12 src tests --ignore=tests/database",

src/EloquentMiddleware.php

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonMapper\EloquentMiddleware;
6+
7+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
8+
use Illuminate\Database\Eloquent\Model;
9+
use JsonMapper\Builders\PropertyBuilder;
10+
use JsonMapper\Enums\Visibility;
11+
use JsonMapper\JsonMapperInterface;
12+
use JsonMapper\Middleware\AbstractMiddleware;
13+
use JsonMapper\ValueObjects\PropertyMap;
14+
use JsonMapper\Wrapper\ObjectWrapper;
15+
use Psr\SimpleCache\CacheInterface;
16+
17+
/**
18+
* Heavily based on the work of Barry vd. Heuvel in https://github.com/barryvdh/laravel-ide-helper
19+
*/
20+
class EloquentMiddleware extends AbstractMiddleware
21+
{
22+
private const DOC_BLOCK_REGEX = '/@(?P<name>[A-Za-z_-]+)(?:[ \t]+(?P<value>.*?))?[ \t]*\r?$/m';
23+
24+
/** @var CacheInterface */
25+
private $cache;
26+
/** @var string */
27+
private $dateClass;
28+
29+
public function __construct(CacheInterface $cache)
30+
{
31+
$this->cache = $cache;
32+
33+
$this->dateClass = class_exists(\Illuminate\Support\Facades\Date::class)
34+
? '\\' . get_class(\Illuminate\Support\Facades\Date::now())
35+
: '\Illuminate\Support\Carbon';
36+
}
37+
38+
public function handle(
39+
\stdClass $json,
40+
ObjectWrapper $object,
41+
PropertyMap $propertyMap,
42+
JsonMapperInterface $mapper
43+
): void {
44+
$inner = $object->getObject();
45+
if (! $inner instanceof Model) {
46+
return;
47+
}
48+
49+
if ($this->cache->has($object->getName())) {
50+
$propertyMap->merge($this->cache->get($object->getName()));
51+
return;
52+
}
53+
54+
$intermediatePropertyMap = $this->fetchPropertyMapForEloquentModel($inner);
55+
$this->cache->set($object->getName(), $intermediatePropertyMap);
56+
$propertyMap->merge($intermediatePropertyMap);
57+
}
58+
59+
private function fetchPropertyMapForEloquentModel(Model $object): PropertyMap
60+
{
61+
$intermediatePropertyMap = new PropertyMap();
62+
63+
$this->discoverPropertiesFromTable($object, $intermediatePropertyMap);
64+
65+
if (method_exists($object, 'getCasts')) {
66+
$this->discoverPropertiesCasts($object, $intermediatePropertyMap);
67+
}
68+
69+
return $intermediatePropertyMap;
70+
}
71+
72+
protected function discoverPropertiesFromTable(Model $model, PropertyMap $propertyMap): void
73+
{
74+
$table = $model->getConnection()->getTablePrefix() . $model->getTable();
75+
$schema = $model->getConnection()->getDoctrineSchemaManager();
76+
$databasePlatform = $schema->getDatabasePlatform();
77+
$databasePlatform->registerDoctrineTypeMapping('enum', 'string');
78+
79+
$database = null;
80+
if (strpos($table, '.')) {
81+
list($database, $table) = explode('.', $table);
82+
}
83+
84+
$columns = $schema->listTableColumns($table, $database);
85+
86+
if (count($columns) === 0) {
87+
return;
88+
}
89+
90+
foreach ($columns as $column) {
91+
$name = $column->getName();
92+
if (in_array($name, $model->getDates())) {
93+
$type = $this->dateClass;
94+
} else {
95+
$type = $column->getType()->getName();
96+
switch ($type) {
97+
case 'string':
98+
case 'text':
99+
case 'date':
100+
case 'time':
101+
case 'guid':
102+
case 'datetimetz':
103+
case 'datetime':
104+
case 'decimal':
105+
$type = 'string';
106+
break;
107+
case 'integer':
108+
case 'bigint':
109+
case 'smallint':
110+
$type = 'integer';
111+
break;
112+
case 'boolean':
113+
switch (config('database.default')) {
114+
case 'sqlite':
115+
case 'mysql':
116+
$type = 'integer';
117+
break;
118+
default:
119+
$type = 'boolean';
120+
break;
121+
}
122+
break;
123+
case 'float':
124+
$type = 'float';
125+
break;
126+
default:
127+
$type = 'mixed';
128+
break;
129+
}
130+
}
131+
132+
$property = PropertyBuilder::new()
133+
->setName($name)
134+
->setType($type)
135+
->setIsNullable(!$column->getNotnull())
136+
->setVisibility(Visibility::PUBLIC())
137+
->setIsArray(false)
138+
->build();
139+
$propertyMap->addProperty($property);
140+
}
141+
}
142+
143+
protected function discoverPropertiesCasts(Model $model, PropertyMap $propertyMap): void
144+
{
145+
$casts = $model->getCasts();
146+
foreach ($casts as $name => $type) {
147+
switch ($type) {
148+
case 'boolean':
149+
case 'bool':
150+
$realType = 'boolean';
151+
break;
152+
case 'string':
153+
$realType = 'string';
154+
break;
155+
case 'array':
156+
case 'json':
157+
$realType = 'array';
158+
break;
159+
case 'object':
160+
$realType = 'object';
161+
break;
162+
case 'int':
163+
case 'integer':
164+
case 'timestamp':
165+
$realType = 'integer';
166+
break;
167+
case 'real':
168+
case 'double':
169+
case 'float':
170+
$realType = 'float';
171+
break;
172+
case 'date':
173+
case 'datetime':
174+
$realType = $this->dateClass;
175+
break;
176+
case 'collection':
177+
$realType = '\Illuminate\Support\Collection';
178+
break;
179+
default:
180+
$realType = class_exists($type) ? ('\\' . $type) : 'mixed';
181+
break;
182+
}
183+
184+
if (! $propertyMap->hasProperty($name)) {
185+
continue;
186+
}
187+
188+
$realType = $this->checkForCustomLaravelCasts($realType);
189+
190+
$builder = $propertyMap->getProperty($name)->asBuilder();
191+
$property = $builder->setType($realType)->build();
192+
$propertyMap->addProperty($property);
193+
}
194+
}
195+
196+
protected function checkForCustomLaravelCasts(string $type): string
197+
{
198+
if (!class_exists($type) || !interface_exists(CastsAttributes::class)) {
199+
return $type;
200+
}
201+
202+
$reflection = new \ReflectionClass($type);
203+
204+
if (!$reflection->implementsInterface(CastsAttributes::class)) {
205+
return $type;
206+
}
207+
208+
$methodReflection = new \ReflectionMethod($type, 'get');
209+
$docComment = $methodReflection->getDocComment() ?: '';
210+
211+
return $this->getReturnTypeFromReflection($methodReflection)
212+
?: $this->getReturnTypeFromDocBlock($docComment)
213+
?: $type;
214+
}
215+
216+
protected function getReturnTypeFromReflection(\ReflectionMethod $reflection): ?string
217+
{
218+
$returnType = $reflection->getReturnType();
219+
if (!$returnType) {
220+
return null;
221+
}
222+
223+
$type = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : (string)$returnType;
224+
225+
if (!$returnType->isBuiltin()) {
226+
$type = '\\' . $type;
227+
}
228+
229+
return $type;
230+
}
231+
232+
private function getReturnTypeFromDocBlock(string $docBlock): ?string
233+
{
234+
// Strip away the start "/**' and ending "*/"
235+
if (strpos($docBlock, '/**') === 0) {
236+
$docBlock = substr($docBlock, 3);
237+
}
238+
if (substr($docBlock, -2) === '*/') {
239+
$docBlock = substr($docBlock, 0, -2);
240+
}
241+
$docBlock = trim($docBlock);
242+
243+
$return = null;
244+
if (preg_match_all(self::DOC_BLOCK_REGEX, $docBlock, $matches)) {
245+
for ($x = 0, $max = count($matches[0]); $x < $max; $x++) {
246+
if ($matches['name'][$x] === 'return') {
247+
$return = $matches['value'][$x];
248+
}
249+
}
250+
}
251+
252+
return $return ?: null;
253+
}
254+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonMapper\EloquentMiddleware\Tests\Implementation;
6+
7+
use Illuminate\Database\Eloquent\Model;
8+
9+
class EloquentModel extends Model
10+
{
11+
protected $connection = 'testbench';
12+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace JsonMapper\EloquentMiddleware\Tests\Integration;
6+
7+
use JsonMapper\Cache\NullCache;
8+
use JsonMapper\EloquentMiddleware\EloquentMiddleware;
9+
use JsonMapper\JsonMapperInterface;
10+
use JsonMapper\ValueObjects\PropertyMap;
11+
use JsonMapper\Wrapper\ObjectWrapper;
12+
use JsonMapper\EloquentMiddleware\Tests\Implementation\EloquentModel;
13+
use Orchestra\Testbench\TestCase;
14+
15+
class EloquentMiddlewareTest extends TestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
$this->loadMigrationsFrom(__DIR__ . '/../database/migrations');
22+
}
23+
24+
protected function getEnvironmentSetUp($app)
25+
{
26+
// Setup default database to use sqlite :memory:
27+
$app['config']->set('database.default', 'testbench');
28+
$app['config']->set('database.connections.testbench', [
29+
'driver' => 'sqlite',
30+
'database' => ':memory:',
31+
'prefix' => '',
32+
]);
33+
}
34+
35+
/**
36+
* @covers \JsonMapper\EloquentMiddleware\EloquentMiddleware
37+
*/
38+
public function testColumnsFromTheDatabaseAreReturned(): void
39+
{
40+
$middleware = new EloquentMiddleware(new NullCache());
41+
$propertyMap = new PropertyMap();
42+
$mapper = $this->createMock(JsonMapperInterface::class);
43+
44+
$middleware->handle(new \stdClass(), new ObjectWrapper(new EloquentModel()), $propertyMap, $mapper);
45+
46+
self::assertTrue($propertyMap->hasProperty('id'));
47+
}
48+
}

0 commit comments

Comments
 (0)