Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Core Laravel Package

concrete5/core

The core of Concrete CMS (formerly concrete5), an open-source PHP content management system. Includes MVC architecture, blocks, themes, permissions, in-context editing, and extensibility for building and managing websites.

View on GitHub
Deep Wiki
Context7

Getting Started

Know what you're consuming. concrete5/core is a read-only split of the concrete/ directory from the main Concrete CMS repository. It is the framework itself — application container, Doctrine layer, page/block/theme/package subsystems, admin UI — packaged so Composer can install it. In a real project you almost never composer require it by hand: the concrete5/composer plugin installs this package into the project's concrete/ directory, and the concrete5/concrete5 skeleton wires the bootstrap, application/, packages/, and web root around it.

  1. Start from a project skeleton, not from this package. An empty directory plus concrete5/core gives you no bootstrap, no application/, no dispatcher.
    composer create-project concrete5/concrete5:^9.5 my-site
    cd my-site
    concrete/bin/concrete c5:install --interactive
    
    Run concrete/bin/concrete list on any install to see the commands actually available in your version rather than trusting blog posts.
  2. Target 9.5.7 — it is both the security and the practicality floor. 9.5.5 patched CVE-2026-106551 (CVSS 8.6), so 9.5.4 and earlier are out. But 9.5.5 also shipped a migration (20260914000000, altering PageContainerInstances.containerID) that fails to apply on MySQL 8, and it did not actually fix Composer installs dying on audit failures. Both are fixed in 9.5.7. If you are on 9.5.4 or earlier, go straight to 9.5.7 rather than stepping through 9.5.5/9.5.6.
  3. Confirm which core you have. composer show concrete5/core reports the tag; the package's release metadata drifts (the listing here shows a future-dated release), so pin ^9.5 and read composer.json for the real PHP/Doctrine constraints. Version 9 is PHP 8.1+, Doctrine ORM 2 + DBAL 3, PHP 8 attributes; version 8 is PHP 7.4 + Doctrine annotations. Most online tutorials are v8-era.
  4. Look here first when learning the API:
    • concrete/src/Support/Facade/ — the short list of real entry points (Page, User, Database, Config, Asset, Route, Loader).
    • concrete/config/concrete.php — every config key with defaults, fully commented. New keys land here each release (9.5.5 added a Help System toggle); diff this file after upgrading rather than assuming.
    • concrete/blocks/*/controller.php — dozens of production-grade block controllers to copy from.
    • concrete/src/Entity/ — working Doctrine entities (Page, File, UserInfo, Express) if you need mapping examples.
    • concrete/src/Page/Page.php and concrete/src/Page/PageList.php — the two classes you will touch every day.
  5. Never write inside concrete/. The project-owned directories are application/ (site-specific code, overrides, config, bootstrap) and packages/ (reusable add-ons). Both survive composer update; concrete/ does not.

First useful thing: a block in application/. Handles are snake_case, namespaces are CamelCase, and the path mirrors the namespace.

<?php
// application/blocks/hello/controller.php
namespace Application\Block\Hello;

use Concrete\Core\Block\BlockController;

class Controller extends BlockController
{
    protected $btTable = 'btHello';
    protected $btInterfaceWidth = 420;
    protected $btInterfaceHeight = 320;
    protected $btCacheBlockRecord = true;
    protected $btCacheBlockOutput = true;
    protected $btCacheBlockOutputOnPost = false;

    public function getBlockTypeName(): string { return t('Hello'); }
    public function getBlockTypeDescription(): string { return t('Greets a visitor.'); }

    public function view(): void
    {
        $this->set('name', (string) $this->name);
    }

    public function save($args): void
    {
        parent::save(['name' => trim((string) ($args['name'] ?? ''))]);
    }
}
<?php // application/blocks/hello/view.php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<p>Hello, <?= h($name ?: t('world')) ?>.</p>

Create application/blocks/hello/db.xml (or a package migration on v9) declaring btHello with a bID key plus a name field, then install the block type from Dashboard → Stacks & Blocks → Block Types. Drop it on a page and you have exercised the whole loop: discovery, install, edit form, persistence, rendering, caching.

Implementation Patterns

Controllers, single pages, and views. Anything under application/controllers/ is autoloaded (Application\Controller\…), and a matching file under application/single_pages/ is the view. $this->set() is the only data bridge.

<?php
// application/controllers/single_page/dashboard/reports.php
namespace Application\Controller\SinglePage\Dashboard;

use Concrete\Core\Page\Controller\DashboardPageController;

class Reports extends DashboardPageController
{
    public function view(): void
    {
        $this->set('rows', $this->app->make(ReportService::class)->recent());
    }
}
<?php // application/single_pages/dashboard/reports.php
defined('C5_EXECUTE') or die('Access Denied.');
?>
<h1><?= t('Reports') ?></h1>
<?php foreach ($rows as $row): ?>
    <tr><td><?= h($row->getName()) ?></td></tr>
<?php endforeach ?>

Every view starts with the C5_EXECUTE guard; every dynamic value goes through h(); every user-facing string goes through t(). If you build dashboard pages, know that 9.5.5 improved Express dashboard breadcrumbs and persists panel visibility, and 9.5.7 fixed the user dashboard reporting "Email Validated: Unknown" for validated users — don't reimplement or work around either in your own dashboard controllers.

Routes for JSON/API endpoints. Register in application/bootstrap/app.php (or a package's on_start()), pointing at a controller class and method. Keep the controller thin and delegate to a service so it stays testable.

// application/bootstrap/app.php
use Concrete\Core\Support\Facade\Route;

Route::register('/api/widgets/{id}', 'Application\Controller\Api\Widgets::detail');

If you consume or extend the built-in REST API, re-verify custom transformers and snapshot tests: 9.5.5 added date_public to the Page API transformer and fixed duplicate block updates and image-block link saves. 9.5.7 also stops the API documentation console from popping browser credential prompts, so console-driven exploration is finally usable without hand-authoring requests.

Doctrine entities plus migrations. On v9, express schema with entities and numbered migrations instead of relying on db.xml. Get the entity manager from the container; never construct one.

<?php
namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ORM\Table(name: 'app_widgets')]
class Widget
{
    #[ORM\Id, ORM\Column(type: 'integer'), ORM\GeneratedValue]
    private ?int $id = null;

    #[ORM\Column(type: 'string', length: 255)]
    private string $name = '';

    public function getName(): string { return $this->name; }
}
$em = $this->app->make(\Doctrine\ORM\EntityManagerInterface::class);
$em->persist($widget);
$em->flush();
$widgets = $em->getRepository(Widget::class)->findBy([], ['id' => 'DESC']);

For packages, put migrations in packages/<handle>/migrations/ extending Concrete\Core\Updater\Migrations\AbstractMigration; they run when the package is installed or updated, which is also how you ship schema changes to other people's sites.

Package task controllers no longer need manual registration (9.5.5+). Previously, a package defining background tasks had to register each task controller with the task manager explicitly. As of 9.5.5 that registration is discovered. New packages: don't copy the old $this->taskManager->registerTaskController() boilerplate from tutorials. Existing packages: you can delete it, but test on a current install first — leaving it in place is harmless in most cases, silently relying on it while developing against an older core is not.

Asset registration in blocks and themes. Register once, require by handle. On v9 prefer the container over the legacy static list.

public function view(): void
{
    $al = $this->app->make(\Concrete\Core\Asset\AssetList::class);
    $al->register('javascript', 'app/widget', 'js/widget.js', ['position' => 3], $this->getPackage());
    $al->register('css', 'app/widget', 'css/widget.css', [], $this->getPackage());

    $this->requireAsset('javascript', 'app/widget');
    $this->requireAsset('css', 'app/widget');
}

Themes declare their handle and asset dependencies in application/themes/<handle>/page_theme.php (extending Concrete\Core\Page\Theme\Theme), then live in view.php/default.php templates. Themes control the outer shell; blocks and page templates control the inside. For custom front-end JS, read ConcreteFetch before hand-rolling another fetch() wrapper.

Custom styles (designs) now behave per page version. 9.5.7 stopped custom inline styles from bleeding between page versions and fixed custom styles on global areas. If you maintain themes, page templates, or a CIF import/export workflow, this is a direct change: re-test any styling that previously relied on the leak, and delete any workaround that re-applied styles after a version bump. 9.5.7 also sanitizes inline designs read from a CIF — malformed or hand-edited CSS in an imported design may be silently stripped now, so validate exported CIFs against a 9.5.7 install before shipping them.

Querying content with PageList, not raw SQL. This is the highest-leverage API in the whole package: it composes filtering by page type, parent, attribute, date, and permission into one builder.

$list = new \Concrete\Core\Page\PageList();
$list->filterByPageTypeHandle('blog_post');
$list->filterByAttribute('blog_tags', 'php');
$list->filterByIsApproved(true);
$list->sortByPublicDateDescending();
$list->setItemsPerPage(20);

foreach ($list->getResults() as $page) {
    $title = $page->getCollectionName();
    $image = $page->getAttribute('thumbnail');
}

Attributes are the site-editor-facing extension point: add them to page types, users, files, and sites from the dashboard, then read them with getAttribute() and filter with filterByAttribute(). Custom attribute types live in packages/<handle>/src/Attribute/… (or attributes/ in older layouts). Stale search indexes are now invalidated when page attributes are deleted (9.5.5), and an attribute-less Express entity no longer breaks the Express Entry List edit dialog (9.5.5) — remove any workarounds you wrote for either.

Users, permissions, redirects. Permission checks belong in the controller, not the view.

$u = $this->app->make(\Concrete\Core\User\User::class); // current user in the container
if (!$u->isRegistered()) {
    $this->redirect('/login');
}

if (!$page->getPermissionObject()->canViewPage()) {
    throw new \Concrete\Core\Error\UserMessageException(t('Access denied.'));
}

Use UserMessageException for recoverable user-facing errors — the core renders it as a proper 403/error page instead of a stack trace.

Package structure for anything reusable. packages/<handle>/controller.php extends Concrete\Core\Package\Package, declaring $pkgHandle, $pkgName, $pkgDescription, and install()/uninstall(). Everything site-specific stays in application/; anything you'd install on a second site goes in a package, because packages can carry their own blocks, themes, single pages, entities, migrations, routes, and tasks, and they can be installed through the dashboard or CLI. A v9 package can also use a real composer.json and $pkgAutoloaderRegistries to bring in PSR-4 source and third-party dependencies.

Config and services. Read config through the facade, write through Config::save() only for settings a human might change in the dashboard; put defaults in code under application/config/ so they survive cache clears.

$enabled = Config::get('concrete.cache.enabled');
Config::save('app.feature_flags.new_editor', true);

$this->app->bind(ReportService::class, fn ($app) => new ReportService($app->make('database')));

The container is the extension seam: binding, aliasing, and event listening from application/bootstrap/app.php lets you influence core behavior without touching core files.

Events instead of patches. Hook on_page_view, on_block_add, on_user_login, or your own custom events through the Events facade; dispatched events plus service bindings cover 90% of "I need to change how core behaves" cases without forking. If you integrate with the Calendar, 9.5.7 filled gaps in EventService's dispatched events — re-check the available events before subscribing to something adjacent and inferring behavior from it.

Package structure for translations. Stack translations were missing before 9.5.7. If you built copy-the-stack or manual t() re-wrapping tricks to get translated stacks, retest on 9.5.7 and retire them.

Gotchas and Tips

  • The split repo is not the community repo. It has essentially no dependents and no issue tracker of its own; pull requests and bug reports go to concretecms/concretecms. Treat this package as an artifact, not a project you participate in.
  • Don't edit concrete/ or vendor/concrete5/core/. The concrete5/composer plugin materializes core into concrete/ and overwrites it on every update. Site changes belong in application/; reusable ones in packages/. Use the dashboard's override features (block templates, page templates, element overrides, class overrides) rather than patching core classes.
  • Upgrading to 9.5.5 or 9.5.6 breaks on MySQL 8. Migration 20260914000000 (altering PageContainerInstances.containerID) fails to apply. Fixed in 9.5.7. Do not hand-edit the migration or skip it — jump directly to 9.5.7 and re-run the updater.
  • Composer's security audit could block the install itself — actually fixed in 9.5.7. Earlier 9.5.x releases failed create-project/install outright when Composer auditing was enabled. If your install dies on an audit error before any application code runs, you're on the wrong patch version: upgrade rather than disabling audit globally in composer.json. 9.5.7 also prints why each ignored advisory is ignored, so use audit.ignore with reasons instead of blanket-silencing.
  • enshrined/svg-sanitize moved to 1.0 in 9.5.7. A major bump. If a package of yours depends on it directly (or on its API surface), pin and test; mixed versions in one tree will surface as strange Class not found or signature errors during file/asset handling.
  • Re-check dependency conflicts after 9.5.7. concretecms/monolog-cascade vs symfony/config v6.4.44 was a hard conflict in 9.5.5, and 9.5.7 refreshed Composer dependencies broadly. Read the resolved tree (composer why symfony/config) before blaming your own package.
  • Namespace must match the handle. application/blocks/my_block/ → Application\Block\MyBlock; package my_package → Concrete\Package\MyPackage (or your Composer namespace in v9). Case matters on Linux, and a mismatch produces a "block type not found" with no useful trace.
  • Block output caching masks your edits. $btCacheBlockOutput = true means you can change view.php and see the old markup. During development, turn off cache in Dashboard → System & Settings → Optimization → Cache & Speed Settings, or run concrete/bin/concrete c5:clear-cache. Remember $btCacheBlockOutputOnPost when the block depends on submitted data.
  • Assets are compiled and cached. Theme/package CSS and JS land under application/files/cache/assets. After editing a file, either bump the asset version or clear the cache; otherwise you will debug CSS the browser never received. Design/custom-style changes are especially prone to this after the 9.5.7 per-version styling fix — clear before concluding the fix didn't land.
  • Redis users: deleteAll on the stash driver was broken before 9.5.5. Cache invalidation that appeared to succeed silently left keys behind. If your deploy script relies on a full cache flush against Redis, verify on a current release rather than trusting pre-upgrade behavior.
  • If a PageList suddenly returns nothing on the homepage, check the core version first. A 9.5.4 regression broke page lists set to "Everywhere" on the homepage; fixed in 9.5.5. Don't rewrite filterByParent() when a patch release is the answer.
  • Moving pages could produce duplicate, un-removable canonical URLs (fixed in 9.5.7). If you have code that manages canonical URLs, redirects, or page-move hooks, re-verify it after upgrading and clean up any orphans left by earlier versions before your SEO tooling flags them again.
  • Only one block per page type area was addable before 9.5.7. If you wrote a workaround for adding a second block into a page type, remove it.
  • Social links with mailto: and tel: targets were broken before 9.5.7. If you patched around them, or validate social link URLs in your own theme/block code, re-check.
  • v8 vs v9 API drift is the #1 source of broken copy-paste. DBAL 3 renamed fetchAll() to fetchAllAssociative() and executeQuery() behaves differently; Doctrine annotations became PHP 8 attributes; Core::make()/Loader::helper() are legacy facades while v9 pushes $this->app->make(). When a snippet fails, check the version of the article, not your code.
  • db.xml is not migrations. It's fine for simple block tables and is still honored, but it cannot alter existing columns cleanly and gives you no rollback story. For anything beyond a create-table, ship a migration; otherwise you'll be hand-writing SQL on every customer's database.
  • File ownership, not code, is the usual "it broke after deploy" cause. application/files/, application/config/, packages/, and updates/ must be writable by the PHP user. Running a CLI command as root and then serving as www-data leaves root-owned cache/compiled files that the web process can't rewrite — clear the cache as the web user or fix ownership.
  • defined('C5_EXECUTE') or die('Access Denied.'); at the top of every view. Without it, direct hits to template files execute partial markup. It's the cheapest security guard in the framework.
  • Escape and localize by default. h() for output, t() for strings, and $form/$token helpers for any form that writes. Concrete's CSRF validation is enforced on block and dashboard submissions; a hand-rolled <form> without the token silently fails validation with an unhelpful message.
  • Update migrations don't run from composer update. Pulling a new core or package version only replaces files; schema changes apply through the dashboard updater or the corresponding c5: command. If a table is missing after a deploy, this is why — and it's the same mechanism that must be run to get past the MySQL 8 migration issue.
  • Full-page and block caches are separate from the asset cache and from Doctrine's proxy/cache directories. When behavior is inconsistent between environments, clear all of them and verify application/files/cache is writable before debugging logic.
  • Debug via config, not var_dump. Enable concrete.debug.display_errors and concrete.log.enable in application/config/concrete.php, then read application/logs/. Uncaught exceptions render a generic error page in production, which makes var_dump useless and hides the actual message.
  • Extension points, in order of preference: config override → event listener → service container binding → class override → package → core patch (never). Most requests that look like "modify core" are satisfiable with a page template override or an attribute plus a PageList filter.
  • AI-assisted patches: follow the repo's own rules. 9.5.7 added guidance asking AI tooling for short code comments and no automatic commits. If you let an assistant generate code against core conventions, mirror that: keep comments terse, and review/commit yourself.
  • Testing is possible but not turnkey. Bootstrap the container from the CLI entry point in a PHPUnit setUp() to exercise services and entities in isolation; resist whole-page integration tests, which are slow and cache-sensitive. Core's own suite under concrete/ is the reference for how the app is booted headlessly — 9.5.5 re-enabled a large batch of previously skipped core tests, and 9.5.7 added install/update coverage under strict and empty sql_mode, so the examples there are more trustworthy than in earlier 9.5.x tags.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib