1. Go to this page and download the library: Download cloude/framework library. Choose the download type require.
2. Extract the ZIP file and open the index.php.
3. Add this code to the index.php.
<?php
require_once('vendor/autoload.php');
/* Start to develop here. Best regards https://php-download.com/ */
$router = new Router(basePath: '/api'); // optional, stripped from the URI
$router->get('/users/{id}', $handler); // GET
$router->post('/users', $handler); // POST
$router->any('/*', $handler); // any method
$router->add(['/foo', '/bar'], $handler); // same handler, multiple routes
$router->setNotFound(fn() => ...); // custom 404
$router->dispatch();
$router->get('/users/{id:\d+}', $h); // matches /users/42, not /users/abc
$router->get('/posts/{slug?}', $h); // matches /posts AND /posts/hello
$router->get('/{lang:(es|en)}/...', $h); // enforces 'es' or 'en' in URL
Input::method(); // GET, POST, ...
Input::uri(); // path without query string, no double slashes
Input::get('q'); // $_GET['q'] or null
Input::post('name'); // $_POST['name'] or null
Input::json(); // decodes JSON body into an array
Input::body(); // raw request body
Input::header('User-Agent');
Input::ip(trustProxy: false);
View::setBasePath(__DIR__ . '/views');
View::render('home.html.php', ['title' => 'Hello']); // prints
$html = View::capture('home.html.php', $vars); // returns a string
echo View::e($text); // HTML escape
Schema::foreignKeySql('orders', [
'columns' => ['user_id'],
'references' => 'users',
'on' => ['id'],
// on_delete / on_update omitted
]);
// → ALTER TABLE `orders` ADD CONSTRAINT `fk_orders_user_id`
// FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
// ON DELETE NO ACTION ON UPDATE NO ACTION
use Cloude\Session;
// Typed access — the first call auto-starts the session with hardened defaults.
Session::set('user_id', 42);
$id = Session::get('user_id', $default = null);
Session::has('user_id');
Session::forget('user_id');
$snapshot = Session::all(); // excludes internal flash/CSRF buckets
// Flash — value survives exactly one redirect
Session::flash('success', 'Saved.');
$msg = Session::pullFlash('success'); // null on the originating request,
// 'Saved.' on the next one (then gone)
Session::reflash('success'); // keep it for one more cycle
// CSRF
$token = Session::csrfToken(); // minted on first call, stable per session
if (!Session::checkCsrf((string) Input::post('_csrf'))) {
throw new \Cloude\Http\HttpException(419, 'CSRF token mismatch');
}
// On login (prevents fixation)
Session::regenerate();
// On logout
Session::destroy();
Session::start([], cookieParams: [
'samesite' => 'Strict',
'domain' => '.example.com',
]);
// any subsequent Session::* calls in this request reuse the
// configured params; auto-start no longer fires.
\Cloude\Bootstrap::initPaths(
docroot: __DIR__,
apppath: dirname(__DIR__) . '/app',
// basepath: defaults to dirname(apppath) if omitted
);
\Cloude\Config::configure(APPPATH . '/config');
if (\Cloude\Bootstrap::serveStaticIfExists(DOCROOT)) {
return false;
}
\Cloude\Bootstrap::run(); // reads debug + views from Config
$router = new \Cloude\Router(\Cloude\Config::baseUrl(['example.com', 'localhost']));
// ...routes...
$router->dispatch();
use Cloude\DateTime;
// Static constructors
DateTime::now(); // current moment
DateTime::today(); // today at 00:00:00
DateTime::parse('2026-05-18 14:30'); // throws \InvalidArgumentException on bad input
DateTime::fromTimestamp(1716000000);
// Format shortcuts
$d->toDateString(); // 'Y-m-d' → '2026-05-18'
$d->toTimeString(); // 'H:i:s' → '14:30:00'
$d->toDateTimeString(); // 'Y-m-d H:i:s' → '2026-05-18 14:30:00'
$d->toIsoString(); // 'c' / RFC 3339 → '2026-05-18T14:30:00+02:00'
(string) $d; // 'Y-m-d H:i:s' — MySQL-shaped, drops the offset
// Use toIsoString() when you need timezone in the output.
// Interpolation works: "saved at $d" → "saved at 2026-05-18 …"
// Arithmetic (immutable — always returns a new instance)
$d->addDays(7)->subHours(2)->addMinutes(30);
$d->addWeeks(1); $d->addMonths(1); $d->addYears(1);
$d->subSeconds(10); $d->subDays(3); // ... etc
// Boundaries
$d->startOfDay(); // 00:00:00
$d->endOfDay(); // 23:59:59
$d->startOfMonth(); // 1st of the month at 00:00:00
$d->endOfMonth(); // last of the month at 23:59:59
// Comparisons
$d->isPast(); $d->isFuture();
$d->isToday(); $d->isYesterday(); $d->isTomorrow();
$d->isBefore($other); $d->isAfter($other); $d->isSameDay($other);
// Signed diffs — positive when $other is later than $this
$d->diffInSeconds($other);
$d->diffInMinutes($other);
$d->diffInHours($other);
$d->diffInDays($other);
// English "5 minutes ago" / "in 3 days"
$d->diffForHumans(); // vs now()
$d->diffForHumans($reference); // vs explicit reference (testable)
use Cloude\Http\HttpException;
use Cloude\Http\NotFoundException;
// Idiomatic 404 from a controller:
$book = $repo->find($isbn) ?? throw new NotFoundException("book $isbn");
// Any HTTP status — message goes into the JSON / debug output:
throw new HttpException(403, 'forbidden');
Cache::ok(); // 1d at the CDN, browser revalidates every request
Cache::ok(3600); // 1h at the CDN, browser revalidates every request
Cache::ok(86400, 300); // 1d at the CDN, 5min in the browser
Cache::notFound(); // short CDN TTL on 404
Cache::unavailable(); // no-store + Retry-After on 5xx
if (Cache::conditionalGet(filemtime($path))) {
return; // 304 sent
}
use Cloude\Cli;
use Cloude\TaskRunner;
class ContentTasks
{
/** Rebuild the search index for $country (default es). */
public static function rebuildIndex(array $args): int
{
$country = Cli::option($args, 'country', 'es');
$dryRun = Cli::flag($args, 'dry-run');
Cli::info("rebuilding index for {$country}" . ($dryRun ? ' (dry run)' : ''));
return 0;
}
/** Drop content older than N days (default 90). */
public static function purgeOld(array $args): int
{
$days = (int) (Cli::option($args, 'days') ?? 90);
Cli::info("purging content older than {$days} days");
return 0;
}
}
$runner = new TaskRunner();
$runner->register('ping', fn () => Cli::out('pong'), 'Connectivity check.');
$runner->registerClass('content', ContentTasks::class);
exit($runner->run($argv));
use Cloude\Domain\{ValueObject, AggregateRoot, DomainEvent, DomainException};
final class Money extends ValueObject
{
public function __construct(
public readonly int $amount, // cents
public readonly string $currency,
) {
if ($amount < 0) {
throw new DomainException('Money cannot be negative');
}
}
public function __toString(): string {
return number_format($this->amount / 100, 2) . ' ' . $this->currency;
}
}
final class BookBorrowed implements DomainEvent
{
public function __construct(
public readonly string $isbn,
public readonly string $memberId,
public readonly \DateTimeImmutable $when,
) {}
public function occurredOn(): \DateTimeImmutable { return $this->when; }
}
final class Book extends AggregateRoot
{
public function __construct(
public readonly string $isbn,
public readonly string $title,
private int $copiesAvailable,
) {}
public function borrow(string $memberId): void {
if ($this->copiesAvailable === 0) {
throw new DomainException("No copies of '{$this->title}' available");
}
$this->copiesAvailable--;
$this->recordEvent(new BookBorrowed($this->isbn, $memberId, new \DateTimeImmutable()));
}
}
// Application layer:
$book->borrow($memberId);
$repo->save($book);
foreach ($book->pullDomainEvents() as $event) {
$eventLog->record($event);
}
use Cloude\Testing\DataProvider;
use Cloude\Testing\TestCase;
final class StrTest extends TestCase
{
protected function setUp(): void { /* per-test fixture */ }
protected function tearDown(): void { /* per-test cleanup */ }
public function testSlugAscii(): void
{
self::assertSame('hello-world', \Cloude\Str::slug('Hello World'));
}
public function testInvalidInputThrows(): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('empty');
\Cloude\Str::slug('');
}
#[DataProvider('truthyCases')]
public function testBool(string $input, bool $expected): void
{
self::assertSame($expected, \Cloude\Config::boolEnv($input, false));
}
public static function truthyCases(): array
{
return [
'yes' => ['yes', true],
'no' => ['no', false],
'empty' => ['', false],
];
}
}
use Cloude\Testing\TestCase;
use Cloude\Http\NotFoundException;
final class BorrowingTest extends TestCase
{
public function test_returns_404_when_book_missing(): void
{
$this->useArrayModel(Book::class, []);
$this->assertHttpException(404, function (): void {
Book::find('does-not-exist') ?? throw new NotFoundException('book');
});
}
public function test_borrow_records_event_at_frozen_time(): void
{
$when = $this->freezeTime('2026-05-18 12:00:00');
$this->useArrayModel(Book::class, [['isbn' => '978', 'copies' => 1]]);
$book = Book::find('978');
$book->borrow('member-42');
$events = $book->pullDomainEvents();
$this->assertCount(1, $events);
$this->assertSame($when->getTimestamp(), $events[0]->occurredOn()->getTimestamp());
}
public function test_controller_deletes_user_on_ban(): void
{
$store = $this->useMockModel(User::class, [
['id' => 1, 'email' => 'ada@x', 'active' => 1],
]);
(new BanUserController())->ban(1);
$this->assertModelReceived($store, 'update', times: 1);
$this->assertModelDidNotReceive($store, 'delete');
self::assertSame([1, ['email' => 'ada@x', 'active' => 0]], $store->lastCall('update'));
}
}
Loading please wait ...
Before you can download the PHP files, the dependencies should be resolved. This can take some minutes. Please be patient.