22
33namespace SalemC \TypeScriptifyLaravelModels ;
44
5+ use Illuminate \Database \Eloquent \Casts \AsStringable ;
56use Illuminate \Database \Eloquent \Model ;
67use Illuminate \Support \Facades \DB ;
8+ use Illuminate \Support \Stringable ;
79use Illuminate \Support \Str ;
810
11+ use ReflectionMethod ;
912use Exception ;
1013use 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