├── src ├── UnknownIconException.php ├── functions.php ├── Facades │ └── Icon.php ├── IconDownloader.php ├── IconRenderer.php ├── Icon.php ├── IconServiceProvider.php ├── IconCache.php └── Commands │ └── IconsPreloadCommand.php ├── LICENSE ├── README.md ├── composer.json └── config └── unicon.php /src/UnknownIconException.php: -------------------------------------------------------------------------------- 1 | withOptions(config('unicon.iconify.request_options', [])) 30 | ->get("$prefix/$name.svg") 31 | ->throwUnlessStatus(200) 32 | ->body(); 33 | } catch (\Exception) { 34 | return null; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unicon-rocks/unicon-laravel", 3 | "description": "Universal icon component for Laravel", 4 | "type": "library", 5 | "license": "MIT", 6 | "autoload": { 7 | "psr-4": { 8 | "Unicon\\": "src/" 9 | }, 10 | "files": ["src/functions.php"] 11 | }, 12 | "authors": [ 13 | { 14 | "name": "Nicolas Hedger", 15 | "email": "nicolas@hedger.ch" 16 | } 17 | ], 18 | "minimum-stability": "stable", 19 | "require": { 20 | "php": ">=8.1", 21 | "illuminate/console": "^12|^11|^10", 22 | "illuminate/support": "^12|^11|^10", 23 | "guzzlehttp/guzzle": "^7" 24 | }, 25 | "require-dev": { 26 | "orchestra/testbench": "^8 || ^9 || ^10", 27 | "laravel/pint": "^1.18", 28 | "phpunit/phpunit": "^11 || ^10" 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Unicon\\Test\\": "test/" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "@php vendor/bin/phpunit --colors=always", 37 | "lint": ["@php vendor/bin/phpstan analyse --verbose --ansi"], 38 | "format": "@php vendor/bin/pint --repair" 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": ["Unicon\\IconServiceProvider"], 43 | "aliases": { 44 | "Icon": "Unicon\\Facades\\Icon" 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/IconRenderer.php: -------------------------------------------------------------------------------- 1 | cache->has($prefix, $name)) { 28 | true => $this->cache->pull($prefix, $name), 29 | false => $this->downloader->download($prefix, $name), 30 | }; 31 | 32 | // If the icon was not found in the cache, store it in the cache. 33 | if (! $isInCache && ! is_null($icon)) { 34 | $this->cache->put($prefix, $name, $icon); 35 | } 36 | 37 | return $icon; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Icon.php: -------------------------------------------------------------------------------- 1 | name ? $this->renderer->render($this->name) : ''; 28 | 29 | if (!$svg) { 30 | if (!App::environment(['production'])) { 31 | throw new UnknownIconException($this->name); 32 | } 33 | return ''; 34 | } 35 | 36 | return $this->injectAttributes($svg); 37 | }; 38 | } 39 | 40 | /** 41 | * Injects the attributes into the SVG element. 42 | */ 43 | protected function injectAttributes(string $svg): string 44 | { 45 | return Str::replace( 46 | search: 'merge([\'class\' => config(\'unicon.defaults.class\') ?: null ]) }} ', 48 | subject: $svg, 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/IconServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__ . '/../config/unicon.php', 'unicon'); 17 | 18 | // Bind the IconRenderer to the container 19 | $this->app->bind(IconRenderer::class, function (Application $app) { 20 | return new IconRenderer( 21 | cache: $app->make(IconCache::class), 22 | downloader: $app->make(IconDownloader::class), 23 | ); 24 | }); 25 | 26 | // Register the Blade component 27 | Blade::component( 28 | class: Icon::class, 29 | alias: config('unicon.name', 'icon'), 30 | ); 31 | } 32 | 33 | public function boot(): void 34 | { 35 | if ($this->app->runningInConsole()) { 36 | $this->publishes([ 37 | __DIR__ . '/../config/unicon.php' => config_path('unicon.php'), 38 | ], 'unicon-config'); 39 | 40 | $this->commands([ 41 | Commands\IconsPreloadCommand::class, 42 | ]); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /config/unicon.php: -------------------------------------------------------------------------------- 1 | [ 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Default class names 21 | |-------------------------------------------------------------------------- 22 | | 23 | | The default class names to apply to all rendered icons. 24 | | 25 | */ 26 | 27 | 'class' => '', 28 | 29 | ], 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Cache configuration 34 | |-------------------------------------------------------------------------- 35 | | 36 | | This section contains the configuration options for the icon cache. 37 | | 38 | */ 39 | 40 | 'cache' => [ 41 | 'disk' => [ 42 | 'driver' => 'local', 43 | 'path' => 'icons', 44 | ], 45 | ], 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Component name 50 | |-------------------------------------------------------------------------- 51 | | 52 | | This value is the name that will be used to register the Blade component 53 | | and directive. 54 | | 55 | */ 56 | 57 | 'name' => 'icon', 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Iconify options 62 | |-------------------------------------------------------------------------- 63 | | 64 | | This section contains the configuration options for the Iconify API. 65 | | 66 | */ 67 | 68 | 'iconify' => [ 69 | 'url' => 'https://api.iconify.design', 70 | 'request_options' => [ 71 | 'timeout' => 5, 72 | ], 73 | ], 74 | 75 | ]; 76 | -------------------------------------------------------------------------------- /src/IconCache.php: -------------------------------------------------------------------------------- 1 | getDisk()->put($this->getPath($prefix, $name), $svg); 24 | } 25 | 26 | /** 27 | * Checks if an icon is in the cache. 28 | * 29 | * @param string $prefix The prefix of the icon to check. 30 | * @param string $name The name of the icon to check. 31 | * @return bool True if the icon is in the cache, false otherwise. 32 | */ 33 | public function has(string $prefix, string $name): bool 34 | { 35 | return $this->getDisk()->exists($this->getPath($prefix, $name)); 36 | } 37 | 38 | /** 39 | * Pulls an icon from the cache. 40 | * 41 | * @param string $prefix The prefix of the icon to pull. 42 | * @param string $name The name of the icon to pull. 43 | * @return string|null The SVG string of the icon, or null if the icon 44 | * was not found in the cache. 45 | */ 46 | public function pull(string $prefix, string $name): ?string 47 | { 48 | if (!$this->has($prefix, $name)) { 49 | return null; 50 | } 51 | 52 | return $this->getDisk()->get($this->getPath($prefix, $name)); 53 | } 54 | 55 | public function clear(): void 56 | { 57 | $this->getDisk()->deleteDirectory('icons'); 58 | } 59 | 60 | protected function getDisk(): Filesystem 61 | { 62 | return Storage::disk(config('unicon.cache.disk.driver', 'local')); 63 | } 64 | 65 | protected function getPath(string $prefix, string $name): string 66 | { 67 | $path = config('unicon.cache.disk.path', ''); 68 | 69 | return "$path/$prefix/$name.svg"; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Commands/IconsPreloadCommand.php: -------------------------------------------------------------------------------- 1 | info('Looking for icons to preload in your Blade files...'); 36 | 37 | $this->findAllBladeFiles() 38 | ->flatMap(fn(string $file) => $this->findIconsInFile($file)) 39 | ->unique() 40 | ->sort() 41 | ->each(fn(string $icon) => $this->preloadIcon($icon)); 42 | 43 | return self::SUCCESS; 44 | } 45 | 46 | /** 47 | * Find all Blade files in the application 48 | * 49 | * This method will look for all Blade files in the application and return 50 | * them as an array of paths. 51 | * 52 | * @return Collection 53 | */ 54 | protected function findAllBladeFiles(): Collection 55 | { 56 | return $this->glob('*.blade.php'); 57 | } 58 | 59 | /** 60 | * Recursive glob 61 | * 62 | * This method will recursively glob files using the given pattern. 63 | */ 64 | protected function glob(string $pattern, ?string $root = null): Collection 65 | { 66 | $root ??= base_path(); 67 | 68 | $files = collect(glob($root . '/' . $pattern)); 69 | 70 | collect(glob($root . '/*', GLOB_ONLYDIR))->each(function ($dir) use (&$files, $pattern) { 71 | if (!in_array(basename($dir), ['node_modules', 'vendor'])) { 72 | $files = $files->merge($this->glob($pattern, $dir)); 73 | } 74 | }); 75 | 76 | return $files; 77 | } 78 | 79 | /** 80 | * Find all icons in a Blade file 81 | * 82 | * This method will attempt to find all Unicon icons in a Blade file 83 | * by performing a regex search on the file contents. This implementation 84 | * only works for icons that have been statitcally defined. Variables in 85 | * the name attribute will not be evaluated. 86 | */ 87 | protected function findIconsInFile(string $path): Collection 88 | { 89 | $contents = file_get_contents($path); 90 | 91 | $componentName = Str::snake(config('unicon.name', 'icon')); 92 | 93 | $hasMatches = preg_match_all( 94 | pattern: '/[\s\S]*?)\1/m', 95 | subject: $contents, 96 | matches: $matches, 97 | flags: PREG_SET_ORDER, 98 | ); 99 | 100 | if (!$hasMatches) { 101 | return []; 102 | } 103 | 104 | return collect($matches)->map(fn($match) => $match['name']); 105 | } 106 | 107 | /** 108 | * Preloads an icon 109 | */ 110 | protected function preloadIcon(string $icon): void 111 | { 112 | $this->info("Preloading {$icon}..."); 113 | $this->renderer->render($icon); 114 | } 115 | } 116 | --------------------------------------------------------------------------------