Let us put the chapter together. We will build a tiny script that converts Markdown to safe HTML using an installed package, with our own class loaded by Composer.
Step 1: create the project
mkdir markdown-demo
cd markdown-demo
composer init --name="demo/markdown-demo" --no-interaction
Composer creates a composer.json that looks like this:
{
"name": "demo/markdown-demo",
"require": {}
}
Step 2: install a package
composer require league/commonmark
Composer downloads the package and its dependencies into vendor/, writes composer.lock and adds the package to composer.json:
{
"name": "demo/markdown-demo",
"require": {
"league/commonmark": "^2.10"
}
}
Step 3: add PSR-4 autoloading for your own code
Edit composer.json and add the autoload section:
{
"name": "demo/markdown-demo",
"require": {
"league/commonmark": "^2.10"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Run:
composer dump-autoload
Step 4: write your class
Create src/MarkdownRenderer.php:
<?php
declare(strict_types=1);
namespace App;
use League\CommonMark\CommonMarkConverter;
final class MarkdownRenderer
{
private CommonMarkConverter $converter;
public function __construct()
{
$this->converter = new CommonMarkConverter([
"html_input" => "strip", // remove raw HTML from the input
"allow_unsafe_links" => false, // block javascript: links
]);
}
public function render(string $markdown): string
{
return (string) $this->converter->convert($markdown);
}
}
Two options matter for security. We tell the library to strip raw HTML and to block unsafe links, because Markdown that comes from users must never be able to inject a script.
Step 5: the entry script
Create index.php:
<?php
declare(strict_types=1);
require __DIR__ . "/vendor/autoload.php";
use App\MarkdownRenderer;
$renderer = new MarkdownRenderer();
echo $renderer->render("# Hello\n\nThis is **PHP**.\n\n<script>alert(1)</script>");
Step 6: run it
php index.php
<h1>Hello</h1>
<p>This is <strong>PHP</strong>.</p>
The script tag in the input is gone. Package and class were found by the autoloader, and there is no require for either.
This output comes from a real run of the same code, with league/commonmark installed through Composer.
What you used
| Step | Idea |
|---|---|
composer init, composer require | Dependency management |
composer.json autoload | PSR-4 mapping |
vendor/autoload.php | One-line autoloading |
composer dump-autoload | Regenerate the autoloader |
| Safe options on the library | Never trust user content |
What to commit
.gitignore:
vendor/
Commit composer.json, composer.lock, src/ and index.php. Anyone who clones the project runs composer install.
Next steps
- Add a test with
composer require --dev phpunit/phpunit. - Add a script:
"scripts": {"serve": "php -S localhost:8000"}and runcomposer serve. - Run
composer auditandcomposer outdatedregularly.
Why this matters in Laravel
composer require laravel/sanctum, composer update) is exactly what you did here. The framework is a collection of packages plus a PSR-4 mapping for your own code.Exercise: add another package
Add a second package to the demo: use composer require nesbot/carbon and print today’s date formatted as d F Y with Carbon::now()->format("d F Y"). What changes in composer.json? Which file records the exact version that was installed?
Show the answer
composer.json gets a new "nesbot/carbon": "^x.y" line in require, and composer.lock records the exact version and its dependencies. The new class is available at once, because Composer regenerates vendor/autoload.php: use Carbon\Carbon; and echo Carbon::now()->format("d F Y");.