pollora / colt
Use WordPress backend with Laravel or any PHP framework
Requires
- php: ^8.2
- bordoni/phpass: 0.3.*
- fakerphp/faker: ^1.19
- illuminate/database: ^12.0 || ^13.0
- illuminate/filesystem: ^12.0 || ^13.0
- illuminate/support: ^12.0 || ^13.0
- thunderer/shortcode: ^0.7.3
Requires (Dev)
- laravel/legacy-factories: ^1.0
- mockery/mockery: ^1.4.4
- orchestra/testbench: ^10.0 || ^11.0
- symfony/thanks: ^1.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-07 08:38:30 UTC
README
Colt is a set of Eloquent models that read and write a WordPress database directly: posts, pages, custom post types, meta, taxonomies, menus, options and users. Use WordPress as the admin and content store, and query its data from Laravel (or any Composer-based PHP app) with the query builder you already know, without loading WordPress.
Colt is a fork of Corcel, created by Junior Grossi, kept up to date with current Laravel releases. Corcel's API is preserved, under the
Pollora\Coltnamespace.
Part of Pollora, the Laravel framework for WordPress. In a Pollora project it is already installed: use the
Pollora\Models\*models (Post,Page,Term,User…), which extend Colt's.
Installation
composer require pollora/colt
Requires PHP 8.2+.
| Laravel | Colt |
|---|---|
| 13.x | ^10.0 |
| 12.x | ^10.0 or ^9.0 |
Laravel 11 is covered by the 8.0 branch only (no tagged release). For older Laravel versions, use Corcel.
Quick start
use Pollora\Colt\Model\Post; // All published posts $posts = Post::published()->get(); // A specific post, its title and a custom field $post = Post::find(31); echo $post->title; // alias of post_title echo $post->meta->link; // value of the "link" post meta
Configuration
Laravel
Colt registers its service provider through package auto-discovery. Publish the configuration file:
php artisan vendor:publish --provider="Pollora\Colt\Laravel\ColtServiceProvider"
This creates config/colt.php, where you set the database connection used for the WordPress tables, and register custom post types and shortcodes.
Suppose config/database.php has a connection for WordPress next to the application one:
// config/database.php 'connections' => [ 'mysql' => [ // Laravel database 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'mydatabase', 'username' => 'admin', 'password' => 'secret', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => '', 'strict' => false, 'engine' => null, ], 'wordpress' => [ // WordPress database, used by Colt 'driver' => 'mysql', 'host' => 'localhost', 'database' => 'mydatabase', 'username' => 'admin', 'password' => 'secret', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => 'wp_', 'strict' => false, 'engine' => null, ], ],
Then point Colt at it in config/colt.php:
'connection' => 'wordpress',
Other PHP frameworks
Load the Composer autoloader if it is not already loaded, then connect to the WordPress database:
require __DIR__ . '/vendor/autoload.php'; Pollora\Colt\Database::connect([ 'database' => 'database_name', 'username' => 'username', 'password' => 'pa$$word', 'prefix' => 'wp_', // default is 'wp_' ]);
You can pass any Eloquent connection parameter. These have defaults you can override:
'driver' => 'mysql', 'host' => 'localhost', 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => 'wp_',
Usage
The examples below use the models of the Pollora\Colt\Model namespace (Post, Page, Taxonomy, Menu, Option, User…). If you extend them with your own classes, call your class instead (App\Models\Post::published()).
Posts
// All published posts $posts = Post::published()->get(); $posts = Post::status('publish')->get(); // A specific post $post = Post::find(31); echo $post->post_title;
Your own model classes
Extending Pollora\Colt\Model\Post lets you add your own methods and, if needed, use another connection than Colt's default one:
namespace App\Models; use Pollora\Colt\Model\Post as ColtPost; class Post extends ColtPost { protected $connection = 'foo-bar'; public function customMethod() { // } }
$posts = App\Models\Post::all(); // uses the 'foo-bar' connection
Extending is optional: the Colt models work as they are.
Meta data (custom fields)
Read meta values from any post:
$post = Post::find(31); echo $post->meta->link; // or echo $post->fields->link; // or echo $post->link;
Create or update meta with saveMeta() or saveField(). They return a bool, like Eloquent's save():
$post = Post::find(1); $post->saveMeta('username', 'jgrossi'); // Several at once $post->saveMeta([ 'username' => 'jgrossi', 'url' => 'http://jgrossi.com', ]);
createMeta() and createField() only create, and return the PostMeta instance instead of a bool:
$post = Post::find(1); $postMeta = $post->createMeta('foo', 'bar'); // PostMeta instance $trueOrFalse = $post->saveMeta('foo', 'baz'); // bool
Querying by meta
Any model using the MetaFields trait (Post, User, Term, Comment…) has meta scopes.
// A published post that has a meta key $post = Post::published()->hasMeta('featured_article')->first(); // Matching both key and value $post = Post::published()->hasMeta('username', 'jgrossi')->first(); // Several fields, or just several keys $post = Post::hasMeta(['username' => 'jgrossi'])->first(); $post = Post::hasMeta(['username' => 'jgrossi', 'url' => 'jgrossi.com'])->first(); $post = Post::hasMeta(['username', 'url'])->first();
hasMetaLike() uses SQL LIKE: matching is case-insensitive and % is a wildcard.
// Matches 'J Grossi', 'J GROSSI' and 'j grossi' $post = Post::published()->hasMetaLike('author', 'J GROSSI')->first(); // Also matches 'Junior Grossi' $post = Post::published()->hasMetaLike('author', 'J%GROSSI')->first();
Field aliases
Post defines aliases in its static $aliases array, such as title for post_title and content for post_content:
$post = Post::find(1); $post->title === $post->post_title; // true
Subclasses can add their own; they inherit the parent's:
class A extends \Pollora\Colt\Model\Post { protected static $aliases = [ 'foo' => 'post_foo', ]; } $a = A::find(1); echo $a->foo; echo $a->title; // from Post
Ordering and pagination
Post and User have newest() and oldest() scopes:
$newest = Post::newest()->first(); $oldest = Post::oldest()->first();
Paginate with Eloquent's paginate():
$posts = Post::published()->paginate(5);
{{ $posts->links() }}
Custom post types
Use the type() scope, or a class of your own:
// With type() $videos = Post::type('video')->status('publish')->get(); // With your own class class Video extends \Pollora\Colt\Model\Post { protected $postType = 'video'; } $videos = Video::status('publish')->get();
type() returns Pollora\Colt\Model\Post objects; your own class returns Video objects, with your methods and properties. Meta works the same way:
$stores = Post::type('store')->status('publish')->take(3)->get(); foreach ($stores as $store) { $storeAddress = $store->address; // or $store->meta->address, or $store->fields->address }
Returning your class for a post type
By default, Post::type('video')->first() returns a Post. Map post types to classes and Colt returns your class for that type everywhere, which matters when a collection mixes types (the items of a menu, for example).
In config/colt.php:
'post_types' => [ 'video' => App\Models\Video::class, 'foo' => App\Models\Foo::class, ],
Or at runtime:
Post::registerPostType('video', App\Models\Video::class); // Every item is now a Video instance $videos = Post::type('video')->status('publish')->get();
This also works for the built-in types: register your own class for page or post.
Pages
Pages are a post type: use Post::type('page') or the Page class.
use Pollora\Colt\Model\Page; $page = Page::slug('about')->first(); // or $page = Post::type('page')->slug('about')->first(); echo $page->post_title;
Taxonomies and categories
// Taxonomies of a post $post = Post::find(1); $taxonomy = $post->taxonomies()->first(); echo $taxonomy->taxonomy; // Posts in a term $post = Post::taxonomy('category', 'php')->first(); // A category and its posts $category = Taxonomy::category()->slug('uncategorized')->first(); $category->posts->each(function ($post) { echo $post->post_title; }); // All categories with their posts $categories = Taxonomy::where('taxonomy', 'category')->with('posts')->get();
Post format
Like WordPress's get_post_format():
echo $post->getFormat(); // 'video', etc.
Attachments and revisions
$page = Page::slug('about')->with('attachment')->first(); print_r($page->attachment); // featured image $post = Post::slug('test')->with('revision')->first(); print_r($post->revision); // all revisions
Thumbnails
$post = Post::find(1); // A Pollora\Colt\Model\Meta\ThumbnailMeta instance print_r($post->thumbnail); // Cast to string, it is the URL of the original image echo $post->thumbnail;
size() returns the metadata of a generated size (e.g. thumbnail or medium), or the original image URL when that size does not exist:
if ($post->thumbnail !== null) { /** * [ * 'file' => 'filename-300x300.jpg', * 'width' => 300, * 'height' => 300, * 'mime-type' => 'image/jpeg', * 'url' => 'http://localhost/wp-content/uploads/filename-300x300.jpg', * ] */ print_r($post->thumbnail->size(Pollora\Colt\Model\Meta\ThumbnailMeta::SIZE_THUMBNAIL)); // http://localhost/wp-content/uploads/filename.jpg print_r($post->thumbnail->size('invalid_size')); }
Shortcodes
Registered shortcodes in post_content are rendered when you read $post->content.
From the configuration (Laravel)
Map shortcodes to classes under the shortcodes key of config/colt.php. Each class implements Pollora\Colt\Shortcode, which requires a render() method:
'shortcodes' => [ 'foo' => App\Shortcodes\FooShortcode::class, 'bar' => App\Shortcodes\BarShortcode::class, ],
use Thunder\Shortcode\Shortcode\ShortcodeInterface; class FooShortcode implements \Pollora\Colt\Shortcode { public function render(ShortcodeInterface $shortcode) { return sprintf( 'html-for-shortcode-%s-%s', $shortcode->getName(), $shortcode->getParameter('one') ); } }
At runtime
// [gallery id="1"] Post::addShortcode('gallery', function ($shortcode) { return $shortcode->getName() . '.' . $shortcode->getParameter('id'); }); $post = Post::find(1); echo $post->content;
In Laravel, register runtime shortcodes in the boot() method of a service provider, such as App\Providers\AppServiceProvider.
Parser
Shortcodes are parsed with thunderer/shortcode. The default RegularParser suits most cases. If parsing looks off, switch to WordpressParser, which follows WordPress's shortcode regex more closely, or to a parser of your own, in config/colt.php:
'shortcode_parser' => Thunder\Shortcode\Parser\RegularParser::class, // 'shortcode_parser' => Thunder\Shortcode\Parser\WordpressParser::class,
Outside Laravel, call setShortcodeParser() on any model using the Shortcodes trait, such as Post:
$post->setShortcodeParser(new WordpressParser()); echo $post->content; // parsed with WordpressParser
Options
Option reads and writes the wp_options table:
$siteUrl = Option::get('siteurl'); Option::add('foo', 'bar'); // stored as a string Option::add('baz', ['one' => 'two']); // serialized $options = Option::asArray(); echo $options['siteurl']; $options = Option::asArray(['siteurl', 'home', 'blogname']); echo $options['home'];
Menus
Get a menu by its slug. Its items are a collection of MenuItem objects (posts of type nav_menu_item). Supported items are pages, posts, custom links and categories; instance() returns the object behind an item:
$menu = Menu::slug('primary')->first(); foreach ($menu->items as $item) { echo $item->instance()->title; // a Post echo $item->instance()->name; // a Term echo $item->instance()->link_text; // a custom link }
instance() returns a Post for post items, a Page for page items, a CustomLink for custom items and a Term for category items.
Multi-level menus
MenuItem::parent() returns the parent of an item (Post, Page, CustomLink or Term):
$items = Menu::slug('foo')->first()->items; $parent = $items->first()->parent();
To build levels, group the items by parent with the collection's groupBy(), on $item->parent()->ID.
Users
$users = User::get(); $user = User::find(1); echo $user->user_login;
Authentication
With Laravel
Colt registers a colt user provider. Declare it in config/auth.php so Laravel logs in WordPress users:
'providers' => [ 'users' => [ 'driver' => 'colt', 'model' => Pollora\Colt\Model\User::class, ], ],
Auth::validate([ 'email' => 'admin@example.com', // or 'username' 'password' => 'secret', ]);
For password resets, the Pollora\Colt\Laravel\Auth\ResetsPasswords trait provides a resetPassword() method that stores the new password with WordPress's hashing. Use it in the class that resets passwords, in place of Laravel's own implementation:
use Pollora\Colt\Laravel\Auth\ResetsPasswords as ColtResetsPasswords; class ResetPasswordController extends Controller { use ResetsPasswords, ColtResetsPasswords { ColtResetsPasswords::resetPassword insteadof ResetsPasswords; } }
Without Laravel
Authenticate with AuthUserProvider directly. Both username and email work as credentials:
$userProvider = new Pollora\Colt\Laravel\Auth\AuthUserProvider; $user = $userProvider->retrieveByCredentials(['username' => 'admin']); if (! is_null($user) && $userProvider->validateCredentials($user, ['password' => 'admin'])) { // logged in }
Documentation
How Colt relates to Corcel and to Pollora's models: pollora.dev/compare.
Testing
composer test
The suite uses Pest with an in-memory SQLite database, built from the factories and migrations in tests/database.
A second suite runs against a real WordPress installation, in .wordpress or the path set in WP_PATH (see the wordpress job of .github/workflows/ci.yml):
composer test:wordpress
Contributing
Contributions are welcome: see the contributing guide. Report security issues privately, as described in the security policy.
License
Colt is open-source software licensed under the MIT license. © RuBee group