Start a CorianderPHP Project
This page is the shortest practical path from a fresh CorianderPHP install to a real feature. It does not prescribe what the feature should be. Pick a small first feature from your own project, then use the same structure for routes, controller, view, module, assets, and optional database.
What You Are Building
Choose a feature slug, such as account, catalog, blog, or support. In the examples below, replace feature with your slug and replace Feature with the matching PascalCase name.
Your first feature should use this structure:
src/
Routes/feature.php
Controllers/FeatureController.php
Modules/Feature/FeatureSummary.php
public/public_views/feature/index.php
nodejs/src/feature/index.ts
The goal is to understand where project code belongs.
Update-Safe Rule
Do not put app features inside CorianderCore. Framework updates manage that directory. If you need app-specific routes, controllers, middleware, modules, views, or assets, place them in the project folders shown below.
1. Install Dependencies
From the project root:
composer install
php coriander nodejs install
Composer installs PHP dependencies. The Node command installs the Tailwind and TypeScript tooling used by the nodejs folder.
2. Configure .env
Start with a local environment:
APP_ENV=local
APP_DEBUG=1
APP_TIMEZONE=Europe/Paris
PROJECT_URL=http://localhost
PUBLIC_URL_PREFIX=/public
Use PUBLIC_URL_PREFIX=/public when Apache serves the project root. Leave it empty only when the web server document root is already the public/ directory.
Checkpoint: open your local project URL. CSS should load from /public/assets/css/output.css.
3. Create A Feature Route File
Create a route file for your feature:
php coriander make:route feature
The file should live here:
src/Routes/feature.php
Add a route:
<?php
declare(strict_types=1);
use Controllers\FeatureController;
use CorianderCore\Core\Router\Router;
return static function (Router $router): void {
$router->get('feature', static fn () => (new FeatureController())->index());
};
Why this matters: public/routes.php should stay small. Feature URLs belong in feature route files.
4. Register The Route File
In public/routes.php, include the feature route file:
$featureRoutes = PROJECT_ROOT . '/src/Routes/feature.php';
if (is_file($featureRoutes)) {
(require $featureRoutes)($router);
}
Checkpoint: /feature should now be handled by the framework router. It may fail until the controller exists, but it should no longer be an Apache directory or missing static file issue.
5. Create The Controller
Create the controller:
php coriander make:controller Feature
Then keep it thin:
<?php
declare(strict_types=1);
namespace Controllers;
use CorianderCore\Core\Router\ViewRenderer;
use Modules\Feature\FeatureSummary;
final class FeatureController
{
public function index(): void
{
(new ViewRenderer())->render('feature', [
'summary' => (new FeatureSummary())->items(),
]);
}
}
The controller coordinates the request. It does not build HTML and it does not own reusable business logic.
6. Create A Custom App Module
Create the module folder:
src/Modules/Feature/
Create src/Modules/Feature/FeatureSummary.php:
<?php
declare(strict_types=1);
namespace Modules\Feature;
final class FeatureSummary
{
public function items(): array
{
return [
['label' => 'Routes', 'value' => 'Configured'],
['label' => 'Controller', 'value' => 'Thin'],
['label' => 'View', 'value' => 'Rendered'],
];
}
}
This is a custom app module. It is different from official Coriander modules inside the framework repository. Custom modules are for project-specific behavior and should stay in src/Modules.
7. Create The View
Create the view:
php coriander make:view feature
Expected file:
public/public_views/feature/index.php
Render the data passed by the controller:
<?php
/** @var array<int,array{label:string,value:string}> $summary */
$summary = $summary ?? [];
?>
<section class="px-4 py-10 font-poppins sm:px-6 lg:px-8">
<h1 class="font-concert-one text-5xl text-dark-green dark:text-peach">Feature</h1>
<div class="mt-8 divide-y divide-dark-green/10 dark:divide-peach/10">
<?php foreach ($summary as $item): ?>
<div class="grid gap-2 py-4 md:grid-cols-[12rem_1fr]">
<span class="font-semibold"><?= htmlspecialchars($item['label'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
<span><?= htmlspecialchars($item['value'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') ?></span>
</div>
<?php endforeach; ?>
</div>
</section>
Checkpoint: open /feature. You should see a server-rendered page with the summary rows.
8. Add TypeScript Only For Interaction
If the page needs small interactions, create:
nodejs/src/feature/index.ts
Example:
document.querySelectorAll('[data-feature-action]').forEach((button) => {
button.addEventListener('click', () => {
button.classList.add('opacity-80');
});
});
Build assets:
php coriander nodejs run build-all
The compiled file goes to:
public/assets/js/feature/index.js
The footer automatically loads a matching view script when it exists.
9. Add Middleware When An Area Needs Protection
Use middleware for an entire protected area. Do not repeat the same access rule inside every controller method.
$router->group('admin', [new AdminMiddleware()], static function (Router $router): void {
$router->get('feature', static fn () => (new FeatureController())->index());
});
Project middleware belongs in:
src/Middleware/
10. Add Database Only When You Need Persistence
Do not start every feature with a database. Add it when the feature needs persisted data.
php coriander make:database
php coriander make:migration CreateFeatureTables
php coriander migrate
For simple reads and writes, prefer the safer condition helpers:
SQLManager::findWhere(['id', 'title'], 'topics', ['category_id' => $categoryId]);
SQLManager::updateWhere('topics', ['title' => $title], ['id' => $topicId]);
SQLManager::deleteWhere('topics', ['id' => $topicId]);
Definition Of Done
Your first feature is correctly shaped when:
- Your feature URL is registered in a feature route file.
- The controller renders a view and delegates reusable logic.
- Custom project logic lives in
src/Modules. - Templates live in
public/public_views. - TypeScript lives in
nodejs/srconly when the page needs interaction. - No app feature code was added to
CorianderCore.
Next
Read the focused references when you need details:
Then follow the complete Forum Guided Project to see these ideas connected in a real feature.