Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.38% covered (success)
99.38%
480 / 483
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
IconResolver
99.38% covered (success)
99.38%
480 / 483
66.67% covered (warning)
66.67%
2 / 3
18
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 resolve
100.00% covered (success)
100.00%
476 / 476
100.00% covered (success)
100.00%
1 / 1
13
 fallback
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
1<?php
2
3namespace Apie\Cms;
4
5use Apie\Core\Attributes\CmsIcon;
6use Apie\Core\Identifiers\KebabCaseSlug;
7use ReflectionClass;
8use Symfony\UX\Icons\Exception\IconNotFoundException;
9use Symfony\UX\Icons\IconRegistryInterface;
10
11final class IconResolver
12{
13    public function __construct(
14        private readonly ?IconRegistryInterface $iconRegistry = null
15    ) {
16    }
17
18    /**
19     * @param ReflectionClass<covariant object>|class-string<covariant object>|string $className
20     */
21    public function resolve(ReflectionClass|string $className): string
22    {
23        $shortName = '';
24        if (is_string($className)) {
25            $shortName = $className;
26            if (class_exists($className)) {
27                $className = new ReflectionClass($className);
28            }
29        }
30        if ($className instanceof ReflectionClass) {
31            foreach ($className->getAttributes(CmsIcon::class) as $attribute) {
32                return $attribute->newInstance()->icon;
33            }
34            $shortName = (new ReflectionClass($className))->getShortName();
35        }
36
37        $map = [
38            'account' => 'mdi:account-circle',
39            'activity' => 'mdi:run',
40            'address' => 'mdi:map-marker',
41            'admin' => 'mdi:shield-account',
42            'alert' => 'mdi:alert-circle-outline',
43            'analytics' => 'mdi:chart-line',
44            'api' => 'mdi:api',
45            'application' => 'mdi:application',
46            'approval' => 'mdi:check-decagram',
47            'archive' => 'mdi:archive',
48            'article' => 'mdi:file-document-outline',
49            'asset' => 'mdi:briefcase',
50            'attachment' => 'mdi:paperclip',
51            'audit' => 'mdi:clipboard-check',
52            'auth' => 'mdi:account-check',
53            'authorization' => 'mdi:shield-lock',
54            'automation' => 'mdi:robot',
55            'avatar' => 'mdi:account-circle-outline',
56            'backup' => 'mdi:backup-restore',
57            'badge' => 'mdi:badge-account',
58            'banner' => 'mdi:flag',
59            'batch' => 'mdi:layers',
60            'blog' => 'mdi:post',
61            'bookmark' => 'mdi:bookmark',
62            'brand' => 'mdi:tag',
63            'browser' => 'mdi:web',
64            'bug' => 'mdi:bug',
65            'bulk' => 'mdi:folder-multiple',
66            'calendar' => 'mdi:calendar-month',
67            'camera' => 'mdi:camera',
68            'campaign' => 'mdi:bullhorn',
69            'cart' => 'mdi:cart-outline',
70            'cash' => 'mdi:cash',
71            'catalog' => 'mdi:bookshelf',
72            'category' => 'mdi:folder',
73            'certificate' => 'mdi:certificate',
74            'chart' => 'mdi:chart-bar',
75            'chat' => 'mdi:chat',
76            'checkout' => 'mdi:cash-register',
77            'claim' => 'mdi:file-document-box-check',
78            'class' => 'mdi:school',
79            'cleaning' => 'mdi:broom',
80            'client' => 'mdi:account-multiple',
81            'clipboard' => 'mdi:clipboard',
82            'cloud' => 'mdi:cloud',
83            'collection' => 'mdi:collection',
84            'column' => 'mdi:view-column',
85            'command' => 'mdi:console',
86            'comment' => 'mdi:comment',
87            'communication' => 'mdi:message-text',
88            'community' => 'mdi:account-group',
89            'company' => 'mdi:office-building',
90            'compare' => 'mdi:balance-scale',
91            'compliance' => 'mdi:certificate',
92            'component' => 'mdi:cube-outline',
93            'conference' => 'mdi:microphone',
94            'configuration' => 'mdi:cog',
95            'connection' => 'mdi:link-variant',
96            'contact' => 'mdi:phone',
97            'content' => 'mdi:file-document-box-multiple',
98            'contentblock' => 'mdi:cards-outline',
99            'contract' => 'mdi:handshake',
100            'contractor' => 'mdi:account-hard-hat',
101            'conversion' => 'mdi:swap-vertical',
102            'cookie' => 'mdi:cookie',
103            'copy' => 'mdi:content-copy',
104            'coupon' => 'mdi:ticket-percent',
105            'credentials' => 'mdi:shield-key',
106            'credit' => 'mdi:credit-card-outline',
107            'critical' => 'mdi:alert-circle',
108            'currency' => 'mdi:currency-usd',
109            'currencyexchange' => 'mdi:currency-usd-off',
110            'customer' => 'mdi:account-tie',
111            'customerservice' => 'mdi:headset',
112            'dashboard' => 'mdi:view-dashboard',
113            'database' => 'mdi:database',
114            'debug' => 'mdi:bug-check',
115            'delete' => 'mdi:delete',
116            'delivery' => 'mdi:truck-delivery',
117            'department' => 'mdi:domain',
118            'description' => 'mdi:format-text',
119            'design' => 'mdi:palette',
120            'device' => 'mdi:tablet-cellphone',
121            'dialog' => 'mdi:chat-processing',
122            'directory' => 'mdi:folder-multiple',
123            'discount' => 'mdi:percent',
124            'display' => 'mdi:monitor',
125            'document' => 'mdi:file-document',
126            'documentation' => 'mdi:book-open-page-variant',
127            'download' => 'mdi:download',
128            'draft' => 'mdi:file-document-edit',
129            'driver' => 'mdi:steering',
130            'dropdown' => 'mdi:menu-down',
131            'duration' => 'mdi:timer',
132            'edit' => 'mdi:pencil',
133            'email' => 'mdi:email',
134            'emails' => 'mdi:email-settings',
135            'employee' => 'mdi:account-tie',
136            'engine' => 'mdi:engine',
137            'environment' => 'mdi:weather-partly-cloudy',
138            'equipment' => 'mdi:car-brake-abs',
139            'error' => 'mdi:alert-circle',
140            'exception' => 'mdi:alert-circle',
141            'exchange' => 'mdi:swap-horizontal',
142            'experience' => 'mdi:star-circle',
143            'export' => 'mdi:export-variant',
144            'extension' => 'mdi:puzzle',
145            'faq' => 'mdi:help-circle',
146            'feature' => 'mdi:star-circle',
147            'feedback' => 'mdi:message-draw',
148            'field' => 'mdi:form-textbox',
149            'file' => 'mdi:file',
150            'filter' => 'mdi:filter-variant',
151            'finance' => 'mdi:finance',
152            'firewall' => 'mdi:shield-fire',
153            'fitness' => 'mdi:run-fast',
154            'folder' => 'mdi:folder',
155            'footer' => 'mdi:footer',
156            'form' => 'mdi:form-select',
157            'fund' => 'mdi:bank',
158            'gallery' => 'mdi:album',
159            'gateway' => 'mdi:gateway',
160            'geo' => 'mdi:earth',
161            'gift' => 'mdi:gift',
162            'goal' => 'mdi:bullseye',
163            'grade' => 'mdi:star',
164            'graph' => 'mdi:chart-areaspline',
165            'graphnode' => 'mdi:graph',
166            'grid' => 'mdi:grid',
167            'group' => 'mdi:account-group',
168            'growth' => 'mdi:trending-up',
169            'guide' => 'mdi:book-open-page-variant',
170            'handshake' => 'mdi:handshake',
171            'hardware' => 'mdi:desktop-classic',
172            'header' => 'mdi:header',
173            'headline' => 'mdi:format-header-1',
174            'health' => 'mdi:heart-pulse',
175            'help' => 'mdi:help-circle',
176            'helpcenter' => 'mdi:lifebuoy',
177            'hierarchy' => 'mdi:sitemap',
178            'history' => 'mdi:history',
179            'home' => 'mdi:home',
180            'hotline' => 'mdi:phone-in-talk',
181            'identity' => 'mdi:account-key',
182            'illustration' => 'mdi:vector-point',
183            'image' => 'mdi:image',
184            'immigration' => 'mdi:passport',
185            'impact' => 'mdi:lightning-bolt',
186            'implementation' => 'mdi:check-decagram',
187            'import' => 'mdi:import',
188            'insight' => 'mdi:eye',
189            'insurance' => 'mdi:shield-account',
190            'integration' => 'mdi:puzzle',
191            'intention' => 'mdi:target',
192            'inventory' => 'mdi:warehouse',
193            'invoice' => 'mdi:file-document',
194            'item' => 'mdi:shape',
195            'job' => 'mdi:briefcase',
196            'journal' => 'mdi:book-open-variant',
197            'judgement' => 'mdi:gavel',
198            'key' => 'mdi:key',
199            'keycard' => 'mdi:key-chain',
200            'keyword' => 'mdi:tag-text',
201            'label' => 'mdi:label',
202            'landing' => 'mdi:rocket',
203            'language' => 'mdi:translate',
204            'layout' => 'mdi:view-dashboard-outline',
205            'lead' => 'mdi:account-star',
206            'legal' => 'mdi:gavel',
207            'lesson' => 'mdi:school',
208            'library' => 'mdi:library-shelves',
209            'license' => 'mdi:license',
210            'link' => 'mdi:link',
211            'linkchain' => 'mdi:link-variant',
212            'list' => 'mdi:view-list',
213            'live' => 'mdi:radio-tower',
214            'loader' => 'mdi:loading',
215            'loan' => 'mdi:cash-plus',
216            'location' => 'mdi:map-marker-radius',
217            'log' => 'mdi:history',
218            'login' => 'mdi:login',
219            'logistics' => 'mdi:truck-fast',
220            'logout' => 'mdi:logout',
221            'lookup' => 'mdi:magnify-scan',
222            'machine' => 'mdi:robot-industrial',
223            'magnet' => 'mdi:magnet',
224            'make' => 'mdi:office-building-outline',
225            'mail' => 'mdi:email-open',
226            'mailbox' => 'mdi:mailbox',
227            'maintenance' => 'mdi:tools',
228            'management' => 'mdi:briefcase-check',
229            'manifest' => 'mdi:file-document-box-check',
230            'map' => 'mdi:map',
231            'market' => 'mdi:storefront',
232            'mask' => 'mdi:face-mask',
233            'media' => 'mdi:play-box-multiple',
234            'meeting' => 'mdi:handshake',
235            'member' => 'mdi:account-multiple-plus',
236            'merchant' => 'mdi:storefront',
237            'message' => 'mdi:message',
238            'metadata' => 'mdi:information-outline',
239            'metric' => 'mdi:thermometer-lines',
240            'microservice' => 'mdi:server',
241            'migration' => 'mdi:database-export',
242            'milestone' => 'mdi:flag-checkered',
243            'mirror' => 'mdi:mirror',
244            'mobile' => 'mdi:cellphone',
245            'model' => 'mdi:shape-outline',
246            'moderation' => 'mdi:gavel',
247            'module' => 'mdi:puzzle',
248            'money' => 'mdi:cash',
249            'moneytransfer' => 'mdi:bank-transfer',
250            'monitor' => 'mdi:monitor',
251            'monitoring' => 'mdi:eye-check',
252            'network' => 'mdi:server-network',
253            'networking' => 'mdi:network',
254            'news' => 'mdi:newspaper',
255            'note' => 'mdi:note-text',
256            'notification' => 'mdi:bell',
257            'notificationcenter' => 'mdi:bell-ring',
258            'number' => 'mdi:counter',
259            'nutrition' => 'mdi:food-apple',
260            'offer' => 'mdi:tag-heart',
261            'office' => 'mdi:office-building',
262            'operation' => 'mdi:cogs',
263            'option' => 'mdi:checkbox-blank-outline',
264            'order' => 'mdi:cart',
265            'organization' => 'mdi:office-building',
266            'overview' => 'mdi:eye',
267            'owner' => 'mdi:account-star-outline',
268            'page' => 'mdi:file-document-box',
269            'pagebuilder' => 'mdi:page-layout-sidebar-right',
270            'paint' => 'mdi:paint',
271            'panel' => 'mdi:monitor-dashboard',
272            'parameter' => 'mdi:tune',
273            'partner' => 'mdi:handshake-outline',
274            'password' => 'mdi:lock',
275            'passwordreset' => 'mdi:lock-reset',
276            'patch' => 'mdi:hammer-wrench',
277            'payment' => 'mdi:credit-card',
278            'paymentmethod' => 'mdi:credit-card-outline',
279            'performance' => 'mdi:speedometer',
280            'permission' => 'mdi:shield-key',
281            'permissiongroup' => 'mdi:shield-account',
282            'personal' => 'mdi:account',
283            'phone' => 'mdi:phone-classic',
284            'photo' => 'mdi:camera-outline',
285            'photogallery' => 'mdi:photo',
286            'phrase' => 'mdi:format-quote-open',
287            'pipeline' => 'mdi:pipe',
288            'plan' => 'mdi:calendar-check',
289            'platform' => 'mdi:web',
290            'policy' => 'mdi:file-chart',
291            'popup' => 'mdi:window-maximize',
292            'portal' => 'mdi:earth',
293            'portfolio' => 'mdi:briefcase-account',
294            'post' => 'mdi:post-outline',
295            'power' => 'mdi:power',
296            'preference' => 'mdi:sliders-horizontal',
297            'preview' => 'mdi:eye-outline',
298            'price' => 'mdi:price-tag',
299            'pricing' => 'mdi:format-list-bulleted-square',
300            'privacy' => 'mdi:shield-lock-outline',
301            'procedure' => 'mdi:clipboard-check-outline',
302            'processor' => 'mdi:cpu-64-bit',
303            'product' => 'mdi:package-variant',
304            'productline' => 'mdi:package-variant-closed',
305            'profile' => 'mdi:account-box',
306            'program' => 'mdi:school-outline',
307            'project' => 'mdi:folder-star',
308            'promotion' => 'mdi:arrow-up-bold',
309            'property' => 'mdi:home-city',
310            'proposal' => 'mdi:file-document-box',
311            'provider' => 'mdi:domain',
312            'publish' => 'mdi:send',
313            'purchase' => 'mdi:cart',
314            'qualification' => 'mdi:certificate-outline',
315            'queue' => 'mdi:format-list-bulleted',
316            'quote' => 'mdi:format-quote-close',
317            'radio' => 'mdi:radio',
318            'radiobutton' => 'mdi:radiobox-marked',
319            'range' => 'mdi:ray-start-end',
320            'rate' => 'mdi:star-rate',
321            'reaction' => 'mdi:emoticon',
322            'receipt' => 'mdi:receipt',
323            'reconcile' => 'mdi:bank-check',
324            'record' => 'mdi:record',
325            'recovery' => 'mdi:refresh-circle',
326            'redirect' => 'mdi:repeat',
327            'redirected' => 'mdi:link-box',
328            'refresh' => 'mdi:refresh',
329            'refund' => 'mdi:cash-refund',
330            'release' => 'mdi:tag-outline',
331            'remote' => 'mdi:remote',
332            'repair' => 'mdi:tools-wrench',
333            'report' => 'mdi:chart-box',
334            'request' => 'mdi:send',
335            'requester' => 'mdi:account-arrow-right',
336            'research' => 'mdi:magnify',
337            'reservation' => 'mdi:calendar-clock',
338            'resource' => 'mdi:database-outline',
339            'response' => 'mdi:reply',
340            'review' => 'mdi:eye-check-outline',
341            'revision' => 'mdi:file-document-box-multiple-outline',
342            'ride' => 'mdi:bike',
343            'rights' => 'mdi:shield-account',
344            'role' => 'mdi:account-badge',
345            'route' => 'mdi:route',
346            'routing' => 'mdi:router-wireless',
347            'sale' => 'mdi:cash-multiple',
348            'salesforce' => 'mdi:cloud',
349            'scale' => 'mdi:scale-balance',
350            'schedule' => 'mdi:calendar-clock',
351            'schema' => 'mdi:schema',
352            'screen' => 'mdi:monitor',
353            'search' => 'mdi:magnify',
354            'secure' => 'mdi:shield-check',
355            'security' => 'mdi:shield-lock',
356            'securitygroup' => 'mdi:shield-group',
357            'segment' => 'mdi:shape',
358            'seller' => 'mdi:store',
359            'service' => 'mdi:tools',
360            'servicelevel' => 'mdi:star-circle-outline',
361            'session' => 'mdi:clock-outline',
362            'sessiontoken' => 'mdi:key-outline',
363            'settings' => 'mdi:cog',
364            'setup' => 'mdi:hammer-wrench',
365            'share' => 'mdi:share-variant',
366            'shipment' => 'mdi:truck',
367            'shortcode' => 'mdi:code-tags',
368            'sidebar' => 'mdi:view-sidebar',
369            'signature' => 'mdi:signature',
370            'site' => 'mdi:web',
371            'sitemap' => 'mdi:sitemap',
372            'skill' => 'mdi:brain',
373            'sms' => 'mdi:message-text-outline',
374            'snapshot' => 'mdi:camera-flip',
375            'social' => 'mdi:share-circle',
376            'source' => 'mdi:source-branch',
377            'spam' => 'mdi:spam',
378            'sponsor' => 'mdi:heart-circle',
379            'sprint' => 'mdi:run-fast',
380            'stage' => 'mdi:stage',
381            'stakeholder' => 'mdi:account-group-outline',
382            'stamp' => 'mdi:stamp',
383            'standard' => 'mdi:check-bold',
384            'startup' => 'mdi:rocket-launch',
385            'state' => 'mdi:map-marker',
386            'statement' => 'mdi:note',
387            'status' => 'mdi:check-circle',
388            'stock' => 'mdi:warehouse',
389            'story' => 'mdi:book',
390            'strategy' => 'mdi:chess-king',
391            'stream' => 'mdi:playlist-play',
392            'structure' => 'mdi:architecture',
393            'student' => 'mdi:school',
394            'study' => 'mdi:mortar-board',
395            'style' => 'mdi:format-paint',
396            'subject' => 'mdi:book-open',
397            'submission' => 'mdi:send-check',
398            'subscription' => 'mdi:bell-ring',
399            'subsystem' => 'mdi:cubes',
400            'success' => 'mdi:check-circle',
401            'summary' => 'mdi:format-list-bulleted',
402            'supplier' => 'mdi:truck-delivery',
403            'support' => 'mdi:lifebuoy',
404            'survey' => 'mdi:clipboard-list',
405            'sync' => 'mdi:sync',
406            'system' => 'mdi:desktop-tower',
407            'tactic' => 'mdi:target',
408            'tag' => 'mdi:tag-multiple',
409            'task' => 'mdi:checkbox-marked',
410            'tasklist' => 'mdi:format-list-checkbox',
411            'team' => 'mdi:account-group',
412            'teammember' => 'mdi:account-multiple-check',
413            'template' => 'mdi:table',
414            'templateitem' => 'mdi:table',
415            'tender' => 'mdi:handshake',
416            'term' => 'mdi:file-document-outline',
417            'territory' => 'mdi:map-marker',
418            'test' => 'mdi:check-all',
419            'ticket' => 'mdi:ticket',
420            'ticketing' => 'mdi:ticket-confirmation',
421            'time' => 'mdi:clock-time-four',
422            'timezone' => 'mdi:clock-time-four-outline',
423            'toast' => 'mdi:bell-outline',
424            'token' => 'mdi:key-star',
425            'tokenization' => 'mdi:key-plus',
426            'tool' => 'mdi:wrench',
427            'topic' => 'mdi:forum',
428            'trade' => 'mdi:swap-horizontal-bold',
429            'transaction' => 'mdi:swap-horizontal',
430            'travel' => 'mdi:airplane',
431            'tree' => 'mdi:tree',
432            'trend' => 'mdi:trending-up',
433            'trip' => 'mdi:map-marker-path',
434            'trophy' => 'mdi:trophy',
435            'trust' => 'mdi:shield-check',
436            'type' => 'mdi:shape',
437            'typeahead' => 'mdi:search-web',
438            'typography' => 'mdi:format-font',
439            'ui' => 'mdi:monitor-dashboard',
440            'uicomponent' => 'mdi:vector-square',
441            'ultimate' => 'mdi:crown',
442            'uncategorized' => 'mdi:tag-off',
443            'underwriting' => 'mdi:shield-check-outline',
444            'uniform' => 'mdi:tshirt-crew',
445            'unlock' => 'mdi:lock-open',
446            'update' => 'mdi:update',
447            'upload' => 'mdi:upload',
448            'uploadhistory' => 'mdi:upload',
449            'user' => 'mdi:account',
450            'usergroup' => 'mdi:account-group',
451            'utility' => 'mdi:wrench',
452            'vacation' => 'mdi:beach',
453            'validation' => 'mdi:check-decagram',
454            'validationrule' => 'mdi:check-decagram',
455            'value' => 'mdi:currency-usd-circle',
456            'valueobject' => 'mdi:hexagon-multiple',
457            'vendor' => 'mdi:store',
458            'vendoraccount' => 'mdi:store-check',
459            'version' => 'mdi:tag-text-outline',
460            'versioncontrol' => 'mdi:source-branch',
461            'video' => 'mdi:video',
462            'videocall' => 'mdi:video-plus',
463            'view' => 'mdi:eye',
464            'viewmode' => 'mdi:eye-outline',
465            'virtual' => 'mdi:cloud',
466            'visitor' => 'mdi:account-eye',
467            'voice' => 'mdi:record-rec',
468            'volume' => 'mdi:volume-high',
469            'voucher' => 'mdi:ticket-percent',
470            'wallet' => 'mdi:wallet',
471            'warning' => 'mdi:alert',
472            'warningtriangle' => 'mdi:alert-outline',
473            'warranty' => 'mdi:shield-check',
474            'watch' => 'mdi:watch',
475            'watermark' => 'mdi:watermark',
476            'web' => 'mdi:web',
477            'webhook' => 'mdi:webhook',
478            'webshop' => 'mdi:cart',
479            'website' => 'mdi:web',
480            'week' => 'mdi:calendar-week',
481            'weight' => 'mdi:scale-balance',
482            'wheel' => 'mdi:wheelchair',
483            'whitepaper' => 'mdi:file-pdf-box',
484            'widget' => 'mdi:widgets',
485            'widgetarea' => 'mdi:widgets',
486            'wifi' => 'mdi:wifi',
487            'workflow' => 'mdi:progress-clock',
488            'workflowengine' => 'mdi:progress-check',
489            'year' => 'mdi:calendar',
490            'yield' => 'mdi:trending-up',
491            'zip' => 'mdi:archive-arrow-down',
492            'zone' => 'mdi:map-marker-multiple',
493            'zoom' => 'mdi:magnify-plus',
494        ];
495
496        foreach ($map as $keyword => $icon) {
497            if (str_contains(strtolower($shortName), $keyword)) {
498                return $icon;
499            }
500        }
501
502        // Generic fallbacks based on naming
503        return match (true) {
504            str_ends_with($shortName, 'Repository') => 'mdi:database',
505            str_ends_with($shortName, 'Controller') => 'mdi:controller',
506            str_ends_with($shortName, 'Form') => 'mdi:form-select',
507            str_ends_with($shortName, 'Dto') => 'mdi:code-braces',
508            str_ends_with($shortName, 'Event') => 'mdi:calendar-star',
509            default => $this->fallback($className, 'mdi:file'),
510        };
511    }
512
513    /**
514     * @param ReflectionClass<covariant object>|string $className
515     */
516    private function fallback(ReflectionClass|string $className, string $fallbackIcon): string
517    {
518        $suffix = is_string($className) ? KebabCaseSlug::fromText($className) : KebabCaseSlug::fromClass($className);
519        if ($this->iconRegistry) {
520            try {
521                $this->iconRegistry->get('mdi:' . $suffix);
522
523                return 'mdi:' . $suffix;
524            } catch (IconNotFoundException) {
525            }
526        }
527        return $fallbackIcon;
528    }
529}