Parse and validate AT Protocol Lexicons with DTO generation for Laravel
1<?php
2
3namespace SocialDept\AtpSchema\Generator;
4
5use SocialDept\AtpSchema\Parser\Nsid;
6
7class NamingConverter
8{
9 /**
10 * Base namespace for generated classes.
11 */
12 protected string $baseNamespace;
13
14 /**
15 * PHP reserved keywords that cannot be used as class names.
16 */
17 protected const RESERVED_KEYWORDS = [
18 'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch',
19 'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do',
20 'echo', 'else', 'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach',
21 'endif', 'endswitch', 'endwhile', 'eval', 'exit', 'extends', 'final',
22 'finally', 'fn', 'for', 'foreach', 'function', 'global', 'goto', 'if',
23 'implements', 'include', 'include_once', 'instanceof', 'insteadof',
24 'interface', 'isset', 'list', 'match', 'namespace', 'new', 'or', 'print',
25 'private', 'protected', 'public', 'readonly', 'require', 'require_once',
26 'return', 'static', 'switch', 'throw', 'trait', 'try', 'unset', 'use',
27 'var', 'while', 'xor', 'yield', '__halt_compiler',
28 ];
29
30 /**
31 * Create a new NamingConverter.
32 */
33 public function __construct(string $baseNamespace = 'App\\Lexicons')
34 {
35 $this->baseNamespace = rtrim($baseNamespace, '\\');
36 }
37
38 /**
39 * Convert NSID to fully qualified class name.
40 */
41 public function nsidToClassName(string $nsidString): string
42 {
43 $nsid = Nsid::parse($nsidString);
44 $namespace = $this->nsidToNamespace($nsidString);
45 $className = $this->toClassName($nsid->getName());
46
47 return $namespace.'\\'.$className;
48 }
49
50 /**
51 * Convert NSID to namespace.
52 */
53 public function nsidToNamespace(string $nsidString): string
54 {
55 $nsid = Nsid::parse($nsidString);
56
57 // Split authority into parts (e.g., "blog.pckt" -> ["blog", "pckt"])
58 $authorityParts = explode('.', $nsid->getAuthority());
59
60 // Convert each part to PascalCase
61 $namespaceParts = array_map(
62 fn ($part) => $this->toPascalCase($part),
63 $authorityParts
64 );
65
66 return $this->baseNamespace.'\\'.implode('\\', $namespaceParts);
67 }
68
69 /**
70 * Get class name from NSID name part.
71 */
72 public function toClassName(string $name): string
73 {
74 // Split on dots (e.g., "feed.post" -> "FeedPost")
75 $parts = explode('.', $name);
76
77 $className = implode('', array_map(
78 fn ($part) => $this->toPascalCase($part),
79 $parts
80 ));
81
82 // Check if the class name is a reserved keyword and add suffix
83 if ($this->isReservedKeyword($className)) {
84 $className .= 'Record';
85 }
86
87 return $className;
88 }
89
90 /**
91 * Check if a name is a PHP reserved keyword.
92 */
93 protected function isReservedKeyword(string $name): bool
94 {
95 return in_array(strtolower($name), self::RESERVED_KEYWORDS, true);
96 }
97
98 /**
99 * Convert to PascalCase.
100 */
101 public function toPascalCase(string $string): string
102 {
103 // Split on hyphens, underscores, or existing camelCase boundaries
104 $words = preg_split('/[-_\s]+|(?=[A-Z])/', $string);
105
106 if ($words === false) {
107 $words = [$string];
108 }
109
110 // Capitalize first letter of each word
111 $words = array_map(fn ($word) => ucfirst(strtolower($word)), $words);
112
113 return implode('', $words);
114 }
115
116 /**
117 * Convert to camelCase.
118 */
119 public function toCamelCase(string $string): string
120 {
121 $pascalCase = $this->toPascalCase($string);
122
123 return lcfirst($pascalCase);
124 }
125
126 /**
127 * Convert to snake_case.
128 */
129 public function toSnakeCase(string $string): string
130 {
131 // Insert underscore before capital letters, except at the start
132 $snake = preg_replace('/(?<!^)[A-Z]/', '_$0', $string);
133
134 if ($snake === null) {
135 return strtolower($string);
136 }
137
138 return strtolower($snake);
139 }
140
141 /**
142 * Convert to kebab-case.
143 */
144 public function toKebabCase(string $string): string
145 {
146 return str_replace('_', '-', $this->toSnakeCase($string));
147 }
148
149 /**
150 * Pluralize a word (simple English rules).
151 */
152 public function pluralize(string $word): string
153 {
154 if (str_ends_with($word, 'y')) {
155 return substr($word, 0, -1).'ies';
156 }
157
158 if (str_ends_with($word, 's') || str_ends_with($word, 'x') || str_ends_with($word, 'ch') || str_ends_with($word, 'sh')) {
159 return $word.'es';
160 }
161
162 return $word.'s';
163 }
164
165 /**
166 * Singularize a word (simple English rules).
167 */
168 public function singularize(string $word): string
169 {
170 if (str_ends_with($word, 'ies')) {
171 return substr($word, 0, -3).'y';
172 }
173
174 if (str_ends_with($word, 'ses') || str_ends_with($word, 'xes') || str_ends_with($word, 'ches') || str_ends_with($word, 'shes')) {
175 return substr($word, 0, -2);
176 }
177
178 if (str_ends_with($word, 's') && ! str_ends_with($word, 'ss')) {
179 return substr($word, 0, -1);
180 }
181
182 return $word;
183 }
184
185 /**
186 * Get the base namespace.
187 */
188 public function getBaseNamespace(): string
189 {
190 return $this->baseNamespace;
191 }
192
193 /**
194 * Set the base namespace.
195 */
196 public function setBaseNamespace(string $namespace): void
197 {
198 $this->baseNamespace = rtrim($namespace, '\\');
199 }
200}