Skip to content

Commit bcf4861

Browse files
authored
Merge pull request #2 from SalemC/add-support-for-natively-casted-attributes
Add support for natively casted attributes
2 parents 29f093b + 9dd2abd commit bcf4861

File tree

1 file changed

+122
-8
lines changed

1 file changed

+122
-8
lines changed

src/TypeScriptifyModel.php

Lines changed: 122 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
namespace SalemC\TypeScriptifyLaravelModels;
44

5+
use Illuminate\Database\Eloquent\Casts\AsStringable;
56
use Illuminate\Database\Eloquent\Model;
67
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Stringable;
79
use Illuminate\Support\Str;
810

11+
use ReflectionMethod;
912
use Exception;
1013
use stdClass;
1114

@@ -17,6 +20,13 @@ class TypeScriptifyModel {
1720
*/
1821
private static string|null $fullyQualifiedModelName = null;
1922

23+
/**
24+
* The instantiated model.
25+
*
26+
* @var \Illuminate\Database\Eloquent\Model|null
27+
*/
28+
private static Model|null $model = null;
29+
2030
/**
2131
* The supported database connections.
2232
*
@@ -56,24 +66,95 @@ private static function hasValidModel(): bool {
5666
* @return string
5767
*/
5868
private static function getTableName(): string {
59-
return (new (self::$fullyQualifiedModelName))->getTable();
69+
return (self::$model)->getTable();
6070
}
6171

6272
/**
63-
* Get the mapped TypeScript type of a type.
73+
* Check if the `$columnField` attribute exists in the protected $dates array.
6474
*
65-
* @param stdClass $columnSchema
75+
* @param string $columnField
76+
*
77+
* @return bool
78+
*/
79+
private static function isAttributeCastedInDates(string $columnField): bool {
80+
return in_array($columnField, (self::$model)->getDates(), false);
81+
}
82+
83+
/**
84+
* Check if the `$columnField` attribute has a native type cast.
85+
*
86+
* @param string $columnField
87+
*
88+
* @return bool
89+
*/
90+
private static function isAttributeNativelyCasted(string $columnField): bool {
91+
$model = self::$model;
92+
93+
// If $columnField exists in the $model->casts array.
94+
if ($model->hasCast($columnField)) return true;
95+
96+
// If $columnField exists in the $model->dates array.
97+
if (self::isAttributeCastedInDates($columnField)) return true;
98+
99+
return false;
100+
}
101+
102+
/**
103+
* Map a native casted attribute (casted via $casts/$dates) to a TypeScript type.
104+
*
105+
* @param string $columnField
66106
*
67107
* @return string
68108
*/
69-
private static function getTypeScriptType(stdClass $columnSchema): string {
70-
$columnType = Str::of($columnSchema->Type);
109+
private static function mapNativeCastToTypeScriptType(string $columnField): string {
110+
// If the attribute is casted to a date via $model->dates, it won't exist in the underlying $model->casts array.
111+
// That means if we called `getCastType` with it, it would throw an error because the key wouldn't exist.
112+
// We know dates get serialized to strings, so we can avoid that by short circuiting here.
113+
if (self::isAttributeCastedInDates($columnField)) return 'string';
114+
115+
// The `getCastType` method is protected, therefore we need to use reflection to call it.
116+
$getCastType = new ReflectionMethod(self::$model, 'getCastType');
117+
118+
$castType = Str::of($getCastType->invoke(self::$model, $columnField));
119+
120+
return match (true) {
121+
$castType->is('int') => 'number',
122+
$castType->is('real') => 'number',
123+
$castType->is('date') => 'string',
124+
$castType->is('float') => 'number',
125+
$castType->is('bool') => 'boolean',
126+
$castType->is('double') => 'number',
127+
$castType->is('string') => 'string',
128+
$castType->is('integer') => 'number',
129+
$castType->is('datetime') => 'string',
130+
$castType->is('boolean') => 'boolean',
131+
$castType->is('array') => 'unknown[]',
132+
$castType->is('encrypted') => 'string',
133+
$castType->is('timestamp') => 'string',
134+
$castType->is('immutable_date') => 'string',
135+
$castType->is(AsStringable::class) => 'string',
136+
$castType->is('immutable_datetime') => 'string',
137+
$castType->is('object') => 'Record<string, unknown>',
71138

72-
// @todo sets
73-
$mappedType = match (true) {
139+
$castType->startsWith('decimal') => 'number',
140+
141+
default => 'unknown',
142+
};
143+
}
144+
145+
/**
146+
* Map a database column type to a TypeScript type.
147+
*
148+
* @param \Illuminate\Support\Stringable $columnType
149+
*
150+
* @return string
151+
*/
152+
private static function mapDatabaseTypeToTypeScriptType(Stringable $columnType): string {
153+
return match (true) {
74154
$columnType->startsWith('bit') => 'number',
75155
$columnType->startsWith('int') => 'number',
76156
$columnType->startsWith('dec') => 'number',
157+
$columnType->startsWith('set') => 'string', // @todo generate the exact TypeScript type this can be.
77158
$columnType->startsWith('char') => 'string',
78159
$columnType->startsWith('text') => 'string',
79160
$columnType->startsWith('blob') => 'string',
@@ -105,7 +186,25 @@ private static function getTypeScriptType(stdClass $columnSchema): string {
105186

106187
default => 'unknown',
107188
};
189+
}
190+
191+
/**
192+
* Get the mapped TypeScript type of a type.
193+
*
194+
* @param stdClass $columnSchema
195+
*
196+
* @return string
197+
*/
198+
private static function getTypeScriptType(stdClass $columnSchema): string {
199+
$columnType = Str::of($columnSchema->Type);
200+
201+
if (self::isAttributeNativelyCasted($columnSchema->Field)) {
202+
$mappedType = self::mapNativeCastToTypeScriptType($columnSchema->Field);
203+
} else {
204+
$mappedType = self::mapDatabaseTypeToTypeScriptType($columnType);
205+
}
108206

207+
// We can't do much with an unknown type.
109208
if ($mappedType === 'unknown') return $mappedType;
110209

111210
if ($columnSchema->Null === 'YES') $mappedType .= '|null';
@@ -132,22 +231,37 @@ private static function generateInterface(): string {
132231
return $str;
133232
}
134233

234+
/**
235+
* Initialize this class.
236+
*
237+
* @param string $fullyQualifiedModelName
238+
*
239+
* @return void
240+
*/
241+
private static function initialize(string $fullyQualifiedModelName): void {
242+
self::$fullyQualifiedModelName = $fullyQualifiedModelName;
243+
self::$model = new (self::$fullyQualifiedModelName);
244+
}
245+
135246
/**
136247
* Reset this class.
137248
*
138249
* @return void
139250
*/
140251
private static function reset(): void {
141252
self::$fullyQualifiedModelName = null;
253+
self::$model = null;
142254
}
143255

144256
/**
145257
* Generate the TypeScript interface.
146258
*
259+
* @param string $fullyQualifiedModelName
260+
*
147261
* @return string
148262
*/
149263
public static function generate(string $fullyQualifiedModelName): string {
150-
self::$fullyQualifiedModelName = $fullyQualifiedModelName;
264+
self::initialize($fullyQualifiedModelName);
151265

152266
if (!self::hasValidModel()) {
153267
throw new Exception('That\'s not a valid model!');

0 commit comments

Comments
 (0)