icetea / icecube
Ice Component system for PHP, using IceCube
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/icetea/icecube
Requires
- php: ^8.1
- icetea/icedom: ^1.0.0
Requires (Dev)
- illuminate/console: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- laravel/pint: ^1.0
- pestphp/pest: ^2.0|^3.0|^4.0
Suggests
- illuminate/console: Required for Laravel artisan commands (^10.0|^11.0|^12.0)
- illuminate/support: Required for Laravel integration (^10.0|^11.0|^12.0)
- scssphp/scssphp: Required for SCSS style compilation (^2.0)
README
A PHP Single-File Component (SFC) framework for building reactive, component-based web applications. IceCube allows you to write PHP components with colocated styles and JavaScript in a single file, similar to Vue.js Single File Components.
Goals
IceCube aims to provide:
- Single-File Component Architecture: Write PHP components with their styles and JavaScript in one cohesive file (
.ice.php) - Component Encapsulation: Scoped styles that don't leak to other components
- Reactive Client-Side Behavior: Easy-to-use client-side interactivity with a refs-based system
- Flexible Compilation: Support for different style preprocessors (CSS, SCSS) and bundling strategies (embedded, Vite)
- Performance Optimization: Automatic compilation, caching, and on-demand loading of components
- Developer Experience: Intuitive API for creating interactive components without complex build processes
- Progressive Enhancement: Components work server-side first, with optional client-side enhancements
Architecture
Core Components
1. Component System
-
Component: Abstract base class for all components- Provides unique ID generation for each component instance
- Implements
SafeStringableinterface for seamless HTML rendering
-
SingleFileComponent: Extended base class for SFC pattern- Automatically extracts public properties as component props
- Injects component metadata (
data-icecube,data-props) into rendered HTML - Manages component lifecycle and client-side hydration
2. Parser Layer
-
IceCubeParser: Extracts content from.ice.phpfiles- Separates PHP code from style and script tags
- Supports multiple
<style>tags with optionalglobalattribute - Can parse colocated
.jsfiles separately - Generates content digest for cache invalidation
-
ParsedComponent: Data structure holding parsed component parts
3. Compiler Layer
-
IceCubeCompiler: Main compilation orchestrator- Scans directories for
.ice.phpfiles - Manages autoloading of components
- Compiles components on-demand or in batch
- Writes compiled files only when changed (optimization)
- Scans directories for
-
CompiledComponent: Container for compiled component artifacts
Style Compilers
Implement the StyleCompiler interface:
NestingStyleCompiler: Wraps styles with component selector[data-icecube={name}]ScssStyleCompiler: Compiles SCSS to CSS with automatic scoping
Script Compilers
Implement the ScriptCompiler interface:
EmbedStyleScriptCompiler: Embeds styles directly in JavaScriptViteScriptCompiler: Imports CSS separately for Vite bundling
4. Registry System
IceCubeRegistry: Global component registry- Stores compiled component metadata
- Provides cache loading/storing functionality
- Generates collection of all component styles
- Injects client-side initialization script
5. Cache System
CachedComponent: Lightweight cached component data- Serializable component metadata for production
- Supports PHP's
var_exportfor efficient caching
Compilation Flow
.ice.php file
↓
IceCubeParser
↓
ParsedComponent (PHP + Styles + JS)
↓
IceCubeCompiler
├→ StyleCompiler → compiled.css
├→ ScriptCompiler → compiled.js (with styles)
└→ PHP class → compiled.php
↓
CompiledComponent
↓
IceCubeRegistry
Client-Side Runtime
The registry generates JavaScript that:
- Dynamically imports component scripts on-demand
- Initializes components via
MutationObserver(supports dynamic content) - Provides a
refsproxy for easy DOM element access - Passes component props from server to client
- Tracks component initialization states (
icing→iced)
Style Scoping
Styles are automatically scoped by wrapping them with the component's data attribute:
/* Original */ .button { color: red; } /* Compiled (Nesting) */ [data-icecube="App_Components_Counter"] { .button { color: red; } } /* Or with SCSS compiler */ [data-icecube="App_Components_Counter"] .button { color: red; }
Global styles (marked with <style global>) bypass scoping.
Usage Guide
Basic Setup (Without Vite)
1. Create a Component
Create a file app/Components/Counter.ice.php:
<?php namespace App\Components; use IceTea\IceCube\SingleFileComponent; use IceTea\IceDOM\HtmlNode; class Counter extends SingleFileComponent { public function __construct( public int $initialCount = 0, public string $label = 'Counter', ) {} public function render(): HtmlNode { return _div(['class' => 'counter-container'], [ _h2($this->label), _p([ _span('Count: '), _strong(['class' => 'count-value'], [ _span($this->initialCount)->data_ref('counter'), ]), ]), _div([ _button(['class' => 'btn'], '-')->data_ref('decrementBtn'), _button(['class' => 'btn'], '+')->data_ref('incrementBtn'), ], ['class' => 'button-group']), ]); } } ?> <style> & { max-width: 400px; margin: 2rem auto; padding: 2rem; background: #f8f9fa; } .count-value { font-size: 2rem; color: #007bff; } .button-group { display: flex; gap: 0.5rem; } </style> <script> export default function ({ root, refs, props }) { const { counter, incrementBtn, decrementBtn } = refs; let count = props.initialCount || 0; const render = () => { counter.textContent = count; }; render(); incrementBtn.addEventListener('click', () => { count++; render(); }); decrementBtn.addEventListener('click', () => { count--; render(); }); } </script>
Alternatively, separate the JavaScript into Counter.js:
// app/Components/Counter.js export default function ({ root, refs, props }) { const { counter, incrementBtn, decrementBtn } = refs; // ... rest of the logic }
2. Initialize the Compiler
In your application bootstrap (e.g., bootstrap/app.php):
use IceTea\IceCube\Compiler\IceCubeCompiler; use IceTea\IceCube\Compiler\EmbedStyleScriptCompiler; $compiler = new IceCubeCompiler( prefixClass: 'App\\Components', sourceDir: __DIR__ . '/../app/Components', compiledPhpDir: storage_path('icecube'), compiledAssetsDir: public_path('icecube'), publicUrl: '/icecube', scriptCompiler: new EmbedStyleScriptCompiler() ); // For development: compile on-demand via autoloader (automatic) // For production: pre-compile all components $compiler->scanAndCompile();
3. Render Components in Views
In your Blade template:
<!DOCTYPE html> <html> <head> <title>My App</title> <?= IceCubeRegistry::allStyles() ?> </head> <body> <?= new Counter(initialCount: 10, label: 'My Counter') ?> <?= IceCubeRegistry::iceCubeScript() ?> </body> </html>
4. Production Optimization
For production, use caching:
// During build/deployment use IceTea\IceCube\IceCubeRegistry; $compiler->scanAndCompile(); IceCubeRegistry::storeCache(storage_path('icecube/cache.php'));
// In production bootstrap IceCubeRegistry::loadCache(storage_path('icecube/cache.php'));
Advanced Setup (With Vite)
1. Install Required Dependencies
npm install --save-dev vite glob
# Optional: For reactive state management
npm install @preact/signals
2. Configure Vite
Update vite.config.js to use Vite's glob import for dynamic component loading:
// vite.config.js import { defineConfig } from "vite"; import laravel from "laravel-vite-plugin"; export default defineConfig({ plugins: [ laravel({ input: ["resources/css/app.css", "resources/js/app.js"], refresh: true, }), ], });
3. Create IceCube Loader Script
Create resources/js/icecube.js:
// Use Vite's import.meta.glob for dynamic imports const componentScripts = import.meta.glob("./**/*.js", { eager: false, base: "../../storage/app/public/icecube", }); const initComponent = async (node) => { const name = node.dataset.icecube; if (!name) return; const mod = await componentScripts[`./${name}.js`]?.(); if (!mod) return; const refs = new Proxy( {}, { get: (_, r) => node.querySelector(`[data-ref="${r}"]`) } ); node.dataset.cube = "icing"; await mod.default({ root: node, refs, props: JSON.parse(node.dataset.props || "{}"), }); node.dataset.cube = "iced"; }; (() => { document.querySelectorAll("[data-icecube]").forEach(initComponent); const observer = new MutationObserver((mutations) => { mutations .flatMap((m) => [...m.addedNodes]) .forEach((node) => { if ( node.nodeType === Node.ELEMENT_NODE && node.dataset.icecube !== undefined ) { initComponent(node); node.querySelectorAll?.("[data-icecube]").forEach(initComponent); } }); }); observer.observe(document.documentElement, { childList: true, subtree: true, }); })();
4. Use icecube.js in your app
In blade template:
@vite(['resources/js/app.js', 'resources/js/icecube.js'])
Or in icedom
<?php _head([ _safe(Vite::withEntryPoints(['resources/js/app.js', 'resources/js/icecube.js'])->toHtml()), ])
5. Configure Compiler with Vite Strategy
In your application bootstrap or controller:
use IceTea\IceCube\Compiler\IceCubeCompiler; use IceTea\IceCube\Compiler\ViteScriptCompiler; use IceTea\IceCube\Compiler\ScssStyleCompiler; // Optional: Configure SCSS with custom import paths $styleCompiler = new ScssStyleCompiler(); $scss = $styleCompiler->getCompiler(); $scss->addImportPath(base_path('app/Components')); $compiler = new IceCubeCompiler( prefixClass: 'App\\Components', sourceDir: base_path('app/Components'), compiledPhpDir: storage_path('app/private/icecube'), compiledAssetsDir: storage_path('app/public/icecube'), publicUrl: '/storage/icecube', scriptCompiler: new ViteScriptCompiler(), styleCompiler: $styleCompiler, // Optional: for SCSS support ); $compiler->scanAndCompile(); // When using Vite, skip storing styles in cache (they're handled by Vite) IceCubeRegistry::storeCache( storage_path('app/private/icecube/cache.php'), includeStyles: false );
6. Run Vite Dev Server
npm run dev
With ViteScriptCompiler, compiled JavaScript will import CSS separately:
// Compiled output: storage/app/public/icecube/App_Components_Counter.js import "./App_Components_Counter.css"; export default function ({ root, refs, props }) { // Your component logic }
7. Use NPM Packages in Components
You can import any NPM package in your component JavaScript files:
// app/Components/Counter.js import { signal, effect } from "@preact/signals"; export default function ({ root, refs, props }) { const { counter, incrementBtn, decrementBtn } = refs; const count = signal(props.initialCount || 0); effect(() => { counter.textContent = count.value; }); incrementBtn.addEventListener("click", () => count.value++); decrementBtn.addEventListener("click", () => count.value--); }
Laravel Integration Guide
Quick Setup
1. Register Service Provider
Add IceCubeServiceProvider to config/app.php:
'providers' => [ // ... IceTea\IceCube\Laravel\IceCubeServiceProvider::class, ],
Or for Laravel 11+, add to bootstrap/providers.php:
return [ // ... IceTea\IceCube\Laravel\IceCubeServiceProvider::class, ];
2. Publish Configuration
php artisan vendor:publish --tag=icecube
This creates config/icecube.php and resources/js/icecube.js where you can customize:
- Component namespace and directories
- Compiler strategies (Vite/Embed, SCSS/CSS)
- Cache settings
3. Create Storage Link
php artisan storage:link
4. Use Components in Blade Views
<!DOCTYPE html> <html> <head> <title>IceCube Demo</title> @vite(['resources/js/icecube.js']) </head> <body> <?= new \App\Components\Counter(initialCount: 10, label: 'My Counter') ?> <?= new \App\Components\Counter(initialCount: 100, label: 'Another Counter') ?> </body> </html>
Service Provider Behavior
The IceCubeServiceProvider automatically:
- Development Mode: Compiles components on-demand via autoloader
- Production Mode: Loads pre-compiled components from cache for maximum performance
Production Build Command
Compile all components and generate cache before deployment:
php artisan icecube:compile
This CompileIceCubeCommand scans all .ice.php files, compiles them, and stores the cache.
Note: The command automatically detects if you're using ViteScriptCompiler and will skip storing styles in cache (since Vite handles CSS bundling separately).
Add to your deployment script:
# Deploy script composer install --optimize-autoloader --no-dev php artisan icecube:compile npm run build # Vite handles CSS bundling php artisan config:cache php artisan route:cache
Configuration
Configuration in config/icecube.php:
return [ 'compilers' => [ 'default' => [ 'prefix_class' => 'App\\Components', 'source_dir' => base_path('app/Components'), 'compiled_php_dir' => storage_path('app/private/icecube'), 'compiled_assets_dir' => storage_path('app/public/icecube'), 'public_url' => '/storage/icecube', 'script_compiler' => ViteScriptCompiler::class, 'style_compiler' => ScssStyleCompiler::class, 'cache_enabled' => env('ICECUBE_CACHE', true), 'cache_file' => storage_path('app/private/icecube/cache.php'), ], ], ];
SCSS Import Paths (Optional)
To use SCSS imports like @import './_base.scss', configure in your service provider:
use IceTea\IceCube\Compiler\ScssStyleCompiler; $styleCompiler = app(ScssStyleCompiler::class); $scss = $styleCompiler->getCompiler(); $scss->addImportPath(base_path('app/Components'));
Dynamic Components (HTMX/AJAX)
Components work seamlessly with dynamic content loading:
// Controller public function loadComponent(Request $request) { return (string) new Counter($request->count, 'Dynamic'); }
<!-- View --> <div hx-get="/load-component?count=100" hx-swap="outerHTML"> Load Component </div>
The icecube.js runtime automatically initializes dynamically added components.
Multiple Compiler Configurations
Configure multiple compilers for different component sets in config/icecube.php:
return [ 'compilers' => [ 'main' => [ 'prefix_class' => 'App\\Components', 'source_dir' => base_path('app/Components'), 'compiled_php_dir' => storage_path('app/private/icecube/main'), 'compiled_assets_dir' => storage_path('app/public/icecube/main'), 'public_url' => '/storage/icecube/main', 'script_compiler' => ViteScriptCompiler::class, 'style_compiler' => ScssStyleCompiler::class, 'cache_enabled' => true, 'cache_file' => storage_path('app/private/icecube/main-cache.php'), ], 'admin' => [ 'prefix_class' => 'App\\Admin\\Components', 'source_dir' => base_path('app/Admin/Components'), 'compiled_php_dir' => storage_path('app/private/icecube/admin'), 'compiled_assets_dir' => storage_path('app/public/icecube/admin'), 'public_url' => '/storage/icecube/admin', 'script_compiler' => EmbedStyleScriptCompiler::class, 'style_compiler' => NestingStyleCompiler::class, 'cache_enabled' => true, 'cache_file' => storage_path('app/private/icecube/admin-cache.php'), ], ], ];
Compilers are automatically registered in the Laravel container as icecube.compiler.{name} singletons.
Compile All:
php artisan icecube:compile
Compile Specific:
php artisan icecube:compile --compiler=admin
The IceCubeServiceProvider automatically:
- Registers all compilers in the container
- In development: initializes compilers for on-demand compilation
- In production: loads all caches for maximum performance
Component API
Props System
Public properties are automatically passed to client-side:
class MyComponent extends SingleFileComponent { public function __construct( public string $title, // Passed to client public array $items, // Passed to client private string $secret, // NOT passed to client ) {} }
Refs System
Use data_ref() to mark elements for easy client-side access:
_button('Click me')->data_ref('myButton')
export default function ({ root, refs, props }) { refs.myButton.addEventListener("click", () => { console.log("Clicked!"); }); }
Component Parameters
root: The component's root DOM elementrefs: Proxy object for accessing elements withdata-refattributesprops: Public properties passed from PHP as JSON
Style Scoping
- Regular
<style>tags: Scoped to component <style global>: Applied globally, not scoped
<style> /* Scoped: only affects this component */ .button { color: blue; } </style> <style global> /* Global: affects entire page */ body { margin: 0; } </style>
SCSS Support
To use SCSS, configure the compiler with ScssStyleCompiler:
use IceTea\IceCube\Compiler\ScssStyleCompiler; $compiler = new IceCubeCompiler( // ... other params styleCompiler: new ScssStyleCompiler() );
Then use SCSS syntax in your components:
<style> $primary-color: #007bff; & { background: $primary-color; .nested { color: darken($primary-color, 10%); } } </style>
File Structure
icecube/
├── src/
│ ├── Component.php # Base component class
│ ├── SingleFileComponent.php # SFC base class
│ ├── IceCubeRegistry.php # Component registry
│ ├── Parser/
│ │ ├── IceCubeParser.php # Parses .ice.php files
│ │ └── ParsedComponent.php # Parsed data structure
│ ├── Compiler/
│ │ ├── IceCubeCompiler.php # Main compiler
│ │ ├── CompiledComponent.php # Compiled data structure
│ │ ├── StyleCompiler.php # Style compiler interface
│ │ ├── NestingStyleCompiler.php # CSS nesting strategy
│ │ ├── ScssStyleCompiler.php # SCSS compilation strategy
│ │ ├── ScriptCompiler.php # Script compiler interface
│ │ ├── EmbedStyleScriptCompiler.php # Embed CSS in JS
│ │ └── ViteScriptCompiler.php # Vite-compatible output
│ ├── Cache/
│ │ └── CachedComponent.php # Cached component data
│ └── Laravel/ # Laravel integration
│ ├── IceCubeServiceProvider.php # Service provider
│ ├── CompileIceCubeCommand.php # Artisan command
│ └── config/
│ └── icecube.php # Configuration file
Best Practices
- Component Organization: Keep components in a dedicated directory (e.g.,
app/Components) - Naming Convention: Use PascalCase for component class names and files
- Separation of Concerns: For complex JavaScript, use separate
.jsfiles - Style Scoping: Prefer scoped styles; use global styles sparingly
- Props Validation: Validate and type-hint public properties
- Production Builds: Always pre-compile and cache components in production
- Asset Strategy: Choose between embedded styles or Vite based on your build setup
License
MIT License