A Concrete CMS project grows. Blocks multiply, themes are edited live, and moving functionality between installations becomes a manual copy-paste. Typical scenario: a client orders a "product card" block, then another, then a theme. Six months later — six blocks, two themes, three custom attributes — and none of them transfer to a new site without a dance with a tambourine. The solution is pack everything into one custom package.
Our engineers with 5+ years of experience developing for Concrete CMS help turn scattered work into a single, reusable module. During this time we have implemented over 30 packages for corporate clients — from simple block sets to full-featured modules with REST API, Doctrine entities, and background tasks. Savings on each deployment reach 90% time, reducing total cost of ownership up to 40%. Contact us to discuss your project — consultation is provided within a day.
Savings When Using a Custom Package
Manual installation of each component separately leads to code duplication, errors in database schema updates, and uncontrolled versions. A custom package solves these problems. One controller.php file automatically installs blocks, themes, attributes, page types, and Cron tasks. Compare: manual installation on a new site takes 2 to 5 hours of per-component setup. A package installs in 5 minutes. Time savings — 10–20 times on each deployment.
How a Custom Package Is Structured in Concrete CMS?
A package is just a folder with a clear structure. The main file is controller.php, which describes dependencies and automatically installs all components.
packages/my-package/
controller.php
blocks/
feature-card/
controller.php
db.xml
add.php
edit.php
view.php
themes/
my-theme/
attributes/
color_picker/
controller.php
single_pages/
dashboard/my_package/settings.php
elements/my_package/settings_form.php
jobs/sync_products.php
src/Entity/Order.php
src/Repository/OrderRepository.php
src/Service/OrderService.php
db.xml
Key Package Components
-
controller.php— registers the package, installs blocks, page types, attributes, and Dashboard pages. - Blocks — custom interface elements (feature-card, team-member).
- Themes — global site design.
- Attributes — custom fields for pages and blocks.
- Single pages — settings pages in the Dashboard.
- Cron Jobs — background tasks (synchronization, email sending).
- Doctrine entities — working with tables via ORM.
- REST API — custom endpoints for integrations.
Package Controller
The heart of the package is the Controller class extending Concrete\Core\Package\Package. It describes the installation, upgrade, and removal of components.
<?php
namespace Concrete\Package\MyPackage;
use Concrete\Core\Package\Package;
use Concrete\Core\Page\Single as SinglePage;
use Concrete\Core\Block\BlockType\BlockType;
use Concrete\Core\Page\Type\Type as PageType;
use Concrete\Core\Attribute\Type as AttributeType;
use Concrete\Core\Job\Job;
defined('C5_EXECUTE') or die('Access Denied.');
class Controller extends Package {
protected string $pkgHandle = 'my-package';
protected string $appVersionRequired = '9.0.0';
protected string $pkgVersion = '2.1.0';
public function getPackageName(): string { return t('My Package'); }
public function getPackageDescription(): string { return t('Full functional package for corporate site'); }
public function on_start(): void {
$this->app->make(\Concrete\Package\MyPackage\Routing\RouteRegistrar::class)->register();
$this->app->make('Concrete\Core\Foundation\Service\ProviderList')
->registerProvider(\Concrete\Package\MyPackage\Provider\ServiceProvider::class);
}
public function install(): void {
$pkg = parent::install();
$this->installOrUpgrade($pkg);
}
public function upgrade(): void {
parent::upgrade();
$pkg = $this->getPackageEntity();
$this->installOrUpgrade($pkg);
}
private function installOrUpgrade(\Concrete\Core\Entity\Package $pkg): void {
$this->installBlock('feature-card', $pkg);
$this->installBlock('team-member', $pkg);
$this->installBlock('testimonial', $pkg);
$this->installPageType('service-detail', 'Service Detail', $pkg);
$this->installPageType('team-member', 'Team Member', $pkg);
$this->installPageAttribute('hero_image', 'image', 'Hero Image', $pkg);
$this->installPageAttribute('intro_text', 'text', 'Intro Text', $pkg);
$this->installPageAttribute('meta_description', 'textarea', 'Meta Description', $pkg);
$this->installPageAttribute('show_in_nav', 'boolean', 'Show in Navigation', $pkg);
$sp = SinglePage::add('/dashboard/my_package', $pkg);
if ($sp) $sp->update(['cName' => 'My Package', 'cDescription' => 'Settings']);
$sp = SinglePage::add('/dashboard/my_package/settings', $pkg);
if ($sp) $sp->update(['cName' => 'Settings']);
Job::installByPackage('sync_products', $pkg);
}
private function installBlock(string $handle, $pkg): void {
if (!BlockType::getByHandle($handle)) {
BlockType::installBlockTypeFromPackage($handle, $pkg);
}
}
private function installPageType(string $handle, string $name, $pkg): void {
if (!PageType::getByHandle($handle)) {
PageType::add(['ptHandle' => $handle, 'ptName' => $name, 'ptIsFrequentlyAdded' => 0, 'ptLaunchInComposer' => 1], $pkg);
}
}
private function installPageAttribute(string $handle, string $type, string $name, $pkg): void {
$at = AttributeType::getByHandle($type);
$ak = \Concrete\Core\Attribute\Key\CollectionKey::getByHandle($handle);
if (!$ak) {
\Concrete\Core\Attribute\Key\CollectionKey::add($at, ['akHandle' => $handle, 'akName' => $name], $pkg);
}
}
public function uninstall(): void {
parent::uninstall();
$db = $this->app->make('database')->connection();
$db->executeStatement('DROP TABLE IF EXISTS MyPackageOrders');
}
}
Comparison: Custom Package vs Manual Installation
| Criterion | Custom Package | Manual Installation |
|---|---|---|
| Deployment time | One zip command → install via admin |
Per-component installation |
| Versioning | Unified management via pkgVersion and db.xml |
No version control |
| Portability | Install on any site in 5 minutes | Requires copying each component |
| Support | Automatic database schema update | Manual migrations |
A custom package speeds up deployment 3–5 times and completely eliminates manual assembly errors. We guarantee compatibility with latest Concrete CMS versions.
Package Development Process
- Requirements analysis — determine package composition (blocks, themes, attributes, entities, API).
- Architecture design — database schema, class structure, routes.
- Implementation — writing controller, blocks, entities, tasks.
- Testing — checking installation, upgrade, removal on a test site.
- Documentation — instructions, API description, examples.
- Deployment — packaging, delivery to client.
What's Included in Package Development
- Source code in a Git repository.
- Documentation: installation, setup, administration.
- Team training (1–2 hours).
- 6-month warranty on support and bug fixes.
- Free upgrade when a new CMS version is released (during warranty period).
What Typical Errors Does a Package Solve?
Consider a real case. A client manually developed 6 blocks, but on a new site the blocks didn't display due to missing dependencies in db.xml. It took 3 days to reinstall each block and fix migrations. With a package, we solved it in 2 hours: described all blocks in the controller, added automatic table creation via db.xml, and tested installation on a clean core. No more transfer errors.
When to Order a Custom Package?
A package is justified if you plan to use the same functionality on multiple sites, want to standardize development, or need guaranteed component portability. If the project is one-off and doesn't require extension, manual installation may suffice. But in most corporate scenarios, the package pays for itself by the second deployment.
Package Development Timeline
| Component | Estimate |
|---|---|
| Package controller + installation | 4–8 h |
| 3–5 custom blocks | 3–6 days |
| Theme with 8–12 page types | 2–4 weeks |
| Doctrine entities + CRUD | 2–4 days |
| Dashboard page + settings | 1–2 days |
| REST API (3–5 endpoints) | 2–3 days |
| Cron Jobs (1–3 tasks) | 4–8 h |
| Full corporate package | 8–16 weeks |
Read more about Concrete CMS on Wikipedia.
Order a custom package development — get a consultation within a day. Contact us to discuss your project.







