Developer Docs
Minnow Developer Documentation
Table of Contents
Mail System
The Minnow mail system provides a modern, extensible API for sending emails with support for templates, queueing, and hooks.
Sending Emails
Quick Send
use function Minnow\Core\Mail\minnow_mail;
// Simple usage (like wp_mail)
minnow_mail('user@example.com', 'Subject', 'Message body');
// With headers
minnow_mail(
'user@example.com',
'Welcome',
'<h1>Hello!</h1>',
['Content-Type' => 'text/html', 'From' => 'Admin <admin@example.com>']
);
// Multiple recipients
minnow_mail(
['user1@example.com', 'user2@example.com'],
'Newsletter',
'Check out our updates!'
);
Using the Mailer Class
use Minnow\Core\Mail\Mailer;
use Minnow\Core\Mail\Message;
$message = Message::create()
->to('user@example.com')
->subject('Hello')
->text('Plain text body');
$sent = Mailer::send($message);
if (!$sent) {
$error = Mailer::getLastError();
}
Message Builder
The Message class provides a fluent interface for building emails:
use Minnow\Core\Mail\Message;
$message = Message::create()
->to('user@example.com') // Single recipient
->to(['user1@example.com', 'user2@...']) // Multiple recipients
->addTo('another@example.com') // Add without replacing
->cc('cc@example.com')
->bcc('bcc@example.com')
->from('sender@example.com', 'Sender Name')
->replyTo('reply@example.com')
->subject('Email Subject')
->text('Plain text body') // OR
->html('<h1>HTML body</h1>') // OR
->body($content, $isHtml) // Generic
->header('X-Custom-Header', 'value')
->headers(['X-One' => '1', 'X-Two' => '2'])
->attach('/path/to/file.pdf')
->attach('/path/to/image.jpg', 'custom-name.jpg');
// Send directly from message
$message->send();
// Or via Mailer
Mailer::send($message);
Email Templates
Templates use Mustache-style syntax with automatic HTML escaping.
Template Syntax
<!-- Escaped output (HTML entities encoded) -->
<p>Hello, {{user_name}}!</p>
<!-- Unescaped/raw output (use carefully) -->
<div>{{{html_content}}}</div>
Sending with Templates
use Minnow\Core\Mail\Mailer;
use function Minnow\Core\Mail\minnow_mail_template;
// Using helper function
minnow_mail_template('welcome', 'user@example.com', [
'user_name' => 'John',
'user_email' => 'john@example.com',
'login_url' => 'https://example.com/login',
]);
// Using Mailer class
Mailer::sendTemplate('password-reset', 'user@example.com', [
'user_name' => 'John',
'reset_url' => 'https://example.com/reset?token=...',
'expiry_hours' => 24,
]);
// Using Message builder with template
Message::create()
->to('user@example.com')
->template('welcome', ['user_name' => 'John'])
->send();
Built-in Templates
Located in core/templates/email/:
| Template | Variables | Description |
|---|---|---|
base |
site_name, year, content, subject, preheader |
Base layout wrapper |
welcome |
user_name, user_email, login_url, site_name |
New user welcome |
password-reset |
user_name, reset_url, expiry_hours |
Password reset request |
password-changed |
user_name, changed_at, ip_address, reset_url |
Password change confirmation |
notification |
user_name, message, action_url, action_text |
Generic notification |
email-verification |
user_name, verify_url, expiry_hours |
Email verification |
new-user-admin |
user_name, user_email, registered_at, user_url |
Admin notification |
login-alert |
user_name, login_at, ip_address, location, device, security_url |
Login security alert |
Creating Custom Templates
- Create an HTML file in
core/templates/email/:
<!-- core/templates/email/order-confirmation.html -->
<p>Hi {{user_name}},</p>
<p>Your order <strong>#{{order_id}}</strong> has been confirmed!</p>
<div class="info-box">
<p><strong>Total:</strong> {{order_total}}</p>
<p><strong>Items:</strong> {{item_count}}</p>
</div>
<div class="button-wrapper">
<a href="{{order_url}}" class="button">View Order</a>
</div>
- Use it in code:
Mailer::sendTemplate('order-confirmation', $customerEmail, [
'user_name' => $customer->name,
'order_id' => $order->id,
'order_total' => '$99.00',
'item_count' => 3,
'order_url' => "https://example.com/orders/{$order->id}",
]);
Template CSS Classes
The base template provides these CSS classes for use in content templates:
.button- Primary action button (blue gradient).button-secondary- Secondary button (white with border).button-wrapper- Centered container for buttons.info-box- Gray info box for metadata.warning-box- Yellow warning box for alerts.divider- Horizontal rule.code- Inline code styling
Registering Templates Programmatically
use Minnow\Core\Mail\Template\Registry;
// Register a template from string content
Registry::register('my-template', '<p>Hello {{name}}!</p>');
// Check if template exists
if (Registry::has('welcome')) {
// ...
}
// List all available templates
$templates = Registry::list();
Email Queue
Queue emails for background sending, with support for delays and retries.
Queueing Emails
use Minnow\Core\Mail\Mailer;
use Minnow\Core\Mail\Message;
use function Minnow\Core\Mail\minnow_mail_queue;
// Queue with helper function
minnow_mail_queue('user@example.com', 'Subject', 'Body');
// Queue with delay (1 hour)
minnow_mail_queue('user@example.com', 'Reminder', 'Don\'t forget!', 3600);
// Queue a Message object
$message = Message::create()
->to('user@example.com')
->subject('Hello')
->text('Body');
Mailer::queue($message); // Queue for immediate processing
Mailer::queue($message, delay: 3600); // Queue with 1 hour delay
Mailer::queue($message, maxAttempts: 5); // Custom retry attempts
// Queue directly from Message
$message->queue();
$message->queue(delay: 3600);
// Queue a templated email
Mailer::queueTemplate('welcome', 'user@example.com', [
'user_name' => 'John',
], delay: 0, maxAttempts: 3);
Queue Processing
Queued emails are processed by the cron system or manually via CLI:
# Process pending emails
minnow mail:queue:process
# Process with custom batch size
minnow mail:queue:process --batch=100
# Process and clean up old entries
minnow mail:queue:process --cleanup --cleanup-days=30
Queue Status
minnow mail:queue:status
Output:
Email Queue Status
Pending: 5
Processing: 0
Sent: 142
Failed: 2
Total: 149
Queue Database Schema
Table: {prefix}minnow_email_queue
| Column | Type | Description |
|---|---|---|
| id | BIGINT | Primary key |
| to_addresses | TEXT | JSON array of recipients |
| subject | VARCHAR(255) | Email subject |
| body | LONGTEXT | Email body |
| is_html | TINYINT(1) | Whether body is HTML |
| headers | TEXT | JSON of custom headers |
| attachments | TEXT | JSON of attachment paths |
| template | VARCHAR(100) | Template name (if used) |
| template_vars | TEXT | JSON of template variables |
| status | ENUM | 'pending', 'processing', 'sent', 'failed' |
| attempts | INT | Number of send attempts |
| max_attempts | INT | Maximum retry attempts (default: 3) |
| scheduled_at | DATETIME | When to send |
| sent_at | DATETIME | When successfully sent |
| failed_at | DATETIME | When last failed |
| error | TEXT | Last error message |
| created_at | DATETIME | When queued |
Mail Hooks
Events
use function Minnow\Core\Hook\on;
// Before sending (can inspect/log)
on('minnow.mail.before_send', function(Message $message, &$shouldSend, &$cancelled, &$cancelReason) {
// Log the email
error_log("Sending email to: " . json_encode($message->getTo()));
// Optionally cancel
if ($someCondition) {
$cancelled = true;
$cancelReason = 'Blocked by policy';
}
});
// After successful send
on('minnow.mail.after_send', function(Message $message, string $transportName) {
// Log success
});
// On send failure
on('minnow.mail.send_failed', function(Message $message, string $error, string $transportName) {
// Log failure, alert admin, etc.
});
// When email is queued
on('minnow.mail.queued', function(QueuedMessage $queued, ?Message $message) {
// Track queued emails
});
// Queue processing events
on('minnow.mail.queue.before_process', function(int $batchSize) {});
on('minnow.mail.queue.after_process', function(array $results) {});
on('minnow.mail.queue.sent', function(QueuedMessage $queued) {});
on('minnow.mail.queue.failed', function(QueuedMessage $queued, string $error) {});
Filters
use function Minnow\Core\Hook\onFilter;
// Modify message before sending
onFilter('minnow.mail.message', function(Message $message) {
// Add tracking pixel, modify headers, etc.
$message->header('X-Mailer', 'Minnow/1.0');
return $message;
});
// Use custom transport
onFilter('minnow.mail.transport', function($transport, Message $message) {
// Return a custom TransportInterface implementation
return new MyCustomTransport();
});
// Add custom template paths
onFilter('minnow.mail.template_paths', function(array $paths) {
$paths[] = '/path/to/plugin/templates/email';
return $paths;
});
// Modify template after loading
onFilter('minnow.mail.template_loaded', function(Template $template, string $name) {
// Modify template content
return $template;
});
// Add default template variables
onFilter('minnow.mail.template_variables', function(array $variables) {
$variables['support_email'] = 'support@example.com';
$variables['company_name'] = 'My Company';
return $variables;
});
Hook System
Minnow uses an event-driven architecture with events (actions) and filters.
Events
Events notify listeners that something happened. Listeners cannot modify the event data.
use function Minnow\Core\Hook\on;
use function Minnow\Core\Hook\emit;
use function Minnow\Core\Hook\off;
// Register a listener
on('user.created', function($user) {
// Send welcome email, log, etc.
});
// With priority (lower runs first, default is 10)
on('user.created', function($user) {
// Runs first
}, priority: 1);
on('user.created', function($user) {
// Runs last
}, priority: 100);
// Emit an event
emit('user.created', $user);
// Remove a listener
off('user.created', $callback, priority: 10);
Filters
Filters allow modifying a value as it passes through the filter chain.
use function Minnow\Core\Hook\onFilter;
use function Minnow\Core\Hook\filter;
// Register a filter
onFilter('post.content', function($content, $post) {
// Modify and return
return str_replace('foo', 'bar', $content);
});
// Apply filters
$content = filter('post.content', $rawContent, $post);
Utility Functions
use function Minnow\Core\Hook\hasListener;
use function Minnow\Core\Hook\hasFilter;
use function Minnow\Core\Hook\emitCount;
use function Minnow\Core\Hook\filterCount;
use function Minnow\Core\Hook\isRunning;
use function Minnow\Core\Hook\current;
// Check if listeners exist
if (hasListener('user.created')) { }
if (hasFilter('post.content')) { }
// Count executions
$count = emitCount('user.created');
$count = filterCount('post.content');
// Check current execution
if (isRunning('user.created')) { }
$hookName = current(); // Currently executing hook name
Cron & Scheduling
Built-in Schedules
| Schedule | Interval |
|---|---|
every_5_minutes |
300 seconds |
hourly |
3600 seconds |
twicedaily |
43200 seconds |
daily |
86400 seconds |
weekly |
604800 seconds |
Registering Tasks
use Minnow\Core\Cron\Scheduler;
// Register a recurring task
Scheduler::register(
'my_cleanup_task', // Unique hook name
'daily', // Schedule
[MyClass::class, 'cleanup'], // Callback
[], // Arguments
'Clean up old data' // Description
);
// Schedule a one-time task
Scheduler::scheduleOnce(
'send_reminder',
time() + 3600, // Run in 1 hour
[Mailer::class, 'send'],
[$message]
);
// Add custom schedule
Scheduler::addSchedule('every_30_minutes', 1800);
Running Cron
# Run all due tasks
minnow cron:run
# List registered tasks
minnow cron:list
# View execution history
minnow cron:history
Server cron setup (recommended):
* * * * * cd /path/to/minnow/public && php tools/cli/bin/minnow cron:run -q
CLI Reference
Mail Commands
# Send an email
minnow mail:send --to="user@example.com" --subject="Hello" --text="Body"
minnow mail:send --to="user@example.com" --template="welcome" --vars='{"user_name":"John"}'
minnow mail:send --to="user@example.com" --subject="Test" --text="Hi" --queue
minnow mail:send --to="user@example.com" --subject="Reminder" --text="!" --delay=3600
# Queue management
minnow mail:queue:status
minnow mail:queue:process
minnow mail:queue:process --batch=100 --cleanup
Cron Commands
minnow cron:run # Run due tasks
minnow cron:list # List all tasks
minnow cron:history # View execution history
minnow cron:history --hook=my_task # Filter by hook
User Commands
minnow user:list
minnow user:create
minnow user:password <user_id>
minnow user:login <user_id> # Generate login URL
Database Commands
minnow db:query "SELECT * FROM users LIMIT 5"
Cache Commands
minnow cache:clear
minnow cache:clear --type=object
Plugin Commands
minnow plugin:list
minnow plugin:analyze <plugin-dir>
minnow plugin:generate
minnow plugin:scan
minnow plugin:migrate <wp-plugin>
Core Commands
minnow core:version
minnow core:download
minnow core:install
minnow core:update
Eval Command
Evaluate PHP code in the Minnow context (non-interactive). Entity shorthand classes (Post, User, Option, Term, Comment, Attachment) and $db are available automatically.
# Evaluate an expression
minnow eval "Post::find(1)"
# Evaluate a statement
minnow eval "echo Option::get('siteurl');"
# Read from stdin
echo 'User::all()' | minnow eval
# Read from a file
minnow eval --file=script.php
Expressions are auto-returned (no need for return). Statements like echo, if, foreach, etc. are executed as-is.
Other Commands
minnow shell # Interactive PHP shell
minnow api:get /minnow/v1/posts # Test API endpoints
minnow post-type:list
minnow taxonomy:list
minnow app-password:list <user_id>
minnow app-password:create <user_id> <name>
minnow app-password:delete <uuid>
Configuration
Database Configuration
admin/config.php:
<?php
return [
'db_host' => 'localhost',
'db_name' => 'minnow',
'db_user' => 'root',
'db_password' => '',
'db_prefix' => 'minnow_',
'db_charset' => 'utf8mb4',
];
function config(string $key, mixed $default = null): mixed {
static $config;
if ($config === null) {
$config = include __FILE__;
}
return $config[$key] ?? $default;
}
Environment Variables
MINNOW_CONFIG- Path to config file (overrides default locations)
Directory Structure
public/
├── admin/ # Admin panel
│ └── config.php # Configuration
├── core/ # Core framework
│ ├── Auth/ # Authentication
│ ├── Cron/ # Scheduling
│ ├── Database/ # Database abstraction
│ ├── Entity/ # Data models
│ ├── Hook/ # Event system
│ ├── Mail/ # Email system
│ │ ├── Queue/ # Queue system
│ │ ├── Template/ # Template system
│ │ └── Transport/ # Mail transports
│ ├── Plugin/ # Plugin system
│ ├── PostType/ # Custom post types
│ ├── Shortcode/ # Shortcode processing
│ └── Taxonomy/ # Taxonomies
├── data/ # User data
│ ├── plugins/ # Installed plugins
│ ├── templates/ # Templates
│ │ └── email/ # Email templates
│ ├── themes/ # Themes
│ └── uploads/ # Media uploads
├── docs/ # Documentation
└── tools/
└── cli/ # CLI application
├── bin/minnow # CLI entry point
└── src/Command/ # CLI commands