jdz / htaccessmaker
Build htaccess files
Requires
- php: >=8.2
- jdz/cspmaker: ^1.0
Requires (Dev)
- jdz/data: ^2.0
- phpunit/phpunit: ^11.0
- symfony/yaml: ^7.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PHP library for generating Apache .htaccess files programmatically. Build secure, optimized, and maintainable .htaccess configurations using an object-oriented approach.
I use this library to manage my clients htaccess files based on their needs and requirements. It allows me to quickly generate .htaccess files with the necessary security, performance, and routing rules without manually writing complex Apache directives.
Features
- π Security-First: Security headers (anti-XSS, HSTS, referrer policy), Content Security Policy, URL / SQL / shell injection blocking, basic authentication
- β‘ Performance Optimized: Compression, expiry headers, cookie-free static files
- ποΈ Modular Architecture: Directives, containers and Apache-module wrappers that nest freely
- π§ Flexible Configuration: Array-based
process()configuration or the fluent API - π Type-Safe: Full PHP type declarations and PHPDoc documentation
Installation
composer require jdz/htaccessmaker
Requirements
- PHP 8.2 or higher
jdz/cspmaker^1.0 β builds the Content Security Policy
Quick Start
Basic Usage
<?php use JDZ\HtaccessMaker\HtAccess; use JDZ\HtaccessMaker\Container\AntiXSS; use JDZ\HtaccessMaker\Module\DeflateModule; use JDZ\HtaccessMaker\Module\ExpiresModule; use JDZ\HtaccessMaker\Directive\ServerSignature; $htaccess = new HtAccess(); // Basic directives $htaccess->addDirective(new ServerSignature('Off')); // Security headers $antiXSS = new AntiXSS(); $antiXSS->process([ 'frameOptions' => 'DENY', 'strictTransportSecurity' => 'max-age=31536000; includeSubDomains', ]); $htaccess->addDirective($antiXSS); // Compression $compression = new DeflateModule(); $compression->process([ 'mimeTypes' => ['text/html', 'text/css', 'application/javascript'], ]); $htaccess->addDirective($compression); // Caching rules $expires = new ExpiresModule(); $expires->process([ 'cacheRules' => [ ['mimeType' => 'text/css', 'expiry' => 'access plus 1 year'], ['mimeType' => 'application/javascript', 'expiry' => 'access plus 1 year'], ], ]); $htaccess->addDirective($expires); // Generate .htaccess content file_put_contents(__DIR__ . '/public/.htaccess', $htaccess->toString());
How process() works
Every container and module is configured with process(array $config). The
config is merged over the class defaults (listed in the
Configuration Reference).
- A non-empty config enables the container. An empty
process()call adds nothing β pass['enabled' => true]to apply the defaults alone. NegociationModule,RoutingRewriteandSecurityRewriteare the exceptions: they always apply their defaults, soprocess()with no argument is enough.
Fluent Interface
<?php use JDZ\HtaccessMaker\HtAccess; use JDZ\HtaccessMaker\Directive\Comment; use JDZ\HtaccessMaker\Directive\ServerSignature; use JDZ\HtaccessMaker\Module\ForceSecureRewrite; $https = new ForceSecureRewrite(); $https->process(['excludePaths' => ['/api/webhook']]); $output = (new HtAccess()) ->withComments(true) ->withApacheCompatibility(true) ->addDirective(new Comment('Security Configuration')) ->addDirective(new ServerSignature('Off')) ->addDirective($https) ->toString(); echo $output;
Comments and Apache Compatibility
withComments(false)drops everyCommentdirective and every raw string line that starts with#β except comments forced withsetForceComment(true).- Modules extending
IfModuleprint their directives bare by default.withApacheCompatibility(true)wraps each of them in its<IfModule mod_xxx.c>block (propagated to nested containers).DeflateModuleandExpiresModuleare always wrapped; a single module can be wrapped with->withIgnoreTag(false).
Core Components
Main Classes (JDZ\HtaccessMaker\)
HtAccess- Main class for generating .htaccess filesHtPasswd- Generate .htpasswd files for basic authentication (APR1-MD5); onlyname:hashlines are written, never the clear-text passwordContainer- Base class for grouping related directivesIfModule- Container wrapped in<IfModule β¦>(see above)Directive- Base class for individual Apache directivesEmptyLine- A blank line in the outputCsp- Deprecated backward-compatible shim overJDZ\CspMaker\Policy(keepsaddToGroup()/merge($csp, $overwrite)); usejdz/cspmaker'sCspBuilder/Policydirectly
Containers (JDZ\HtaccessMaker\Container\)
AntiXSS- X-XSS-Protection, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Strict-Transport-SecurityCspContainer- Content-Security-Policy header, built withjdz/cspmakerBrowserRender- Content-Style-Type / Content-Script-Type headers on PHP and HTML filesUaCompatible- X-UA-Compatible header, optionally unset on static filesPreventCookie- Cookie-free, long-cached static files (aFilesMatch)MimeTypes-AddTypedefinitionsErrorDocuments- Custom error pagesFilesMatch-<FilesMatch "pattern">blockLimitExcept-<LimitExcept METHOD β¦>block
Apache Modules (JDZ\HtaccessMaker\Module\)
DeflateModule- Gzip compression (mod_deflate)ExpiresModule- Cache expiry headers by MIME type (mod_expires)BasicAuthModule- HTTP Basic Authentication with allowed paths / user agents (mod_auth_basic)NegociationModule- MultiViews andIndexIgnore(mod_negotiation)RewriteModule- Base URL rewriting (mod_rewrite):addRewriteBase(),addRewriteCond(),addRewriteRule(),addMaintenanceMode()ForceSecureRewrite- Force HTTPS redirectionRedirectWwwRewrite- WWW to non-WWW redirectionMaintenanceRewrite- Maintenance mode with IP whitelistingRoutingRewrite- Trailing slash, versioned files, per-domain apps and front controller routingSecurityRewrite- URL attack, SQL / shell injection, HTTP method, user agent and referrer blocking
Directives (JDZ\HtaccessMaker\Directive\)
AddHandler, AddType, Comment, DirectoryIndex, ErrorDocument,
ExpiresByType, ExpiresDefault, FileETag, Header (withAlways(),
setCondition()), IndexIgnore, Options, RewriteBase, RewriteCond,
RewriteEngine, RewriteRule, ServerSignature, and ValueDirective (the
base for single-value directives).
Advanced Usage
Custom Security Configuration
<?php use JDZ\HtaccessMaker\HtAccess; use JDZ\HtaccessMaker\Module\SecurityRewrite; $htaccess = new HtAccess(); $security = new SecurityRewrite(); $security->process([ 'blockUrlAttacks' => true, 'blockSqlInjection' => true, 'blockShellInjection' => true, 'blockMaliciousRequests' => true, 'blacklistUserAgents' => ['badbot'], ]); $htaccess->addDirective($security);
Maintenance Mode Setup
<?php use JDZ\HtaccessMaker\Module\MaintenanceRewrite; $maintenance = new MaintenanceRewrite(); $maintenance->process([ 'allowedIps' => ['192.168.1.100', '10.0.0.1'], 'maintenanceFile' => '/maintenance.html', 'defaultState' => false, // Default state: off ]); // Generates both ON and OFF sections - just uncomment the needed one
Routing
<?php use JDZ\HtaccessMaker\Module\RoutingRewrite; $routing = new RoutingRewrite(); $routing->process([ 'baseUrl' => '/', 'defaultController' => 'index.php', 'domainApps' => [ ['domain' => 'api.example.com', 'file' => 'api.php'], ], 'rules' => [ ['from' => '^old-page/?$', 'to' => '/new-page/'], // flags default to R=301,L ], ]);
Content Security Policy
<?php use JDZ\HtaccessMaker\Container\CspContainer; $csp = new CspContainer(); $csp->process([ 'csp' => [ 'script' => ['self', 'https://cdn.example.com'], 'frame-ancestors' => ['none'], ], 'integrations' => [ 'googleFonts', ['matomo' => ['host' => 'stats.example.com']], ], ]);
Each csp entry replaces that directive's default; short directive names
(script, img, β¦) and source keywords (self, none, data) are normalized
by jdz/cspmaker. integrations applies its named recipes (matomo,
googleFonts, googleAnalytics, googleTagManager, youtube, recaptcha,
stripe).
Configuration Reference
Defaults in parentheses.
| Class | process() keys |
|---|---|
AntiXSS |
xssProtection (1; mode=block), frameOptions (SAMEORIGIN), contentTypeOptions (nosniff), refererPolicy (strict-origin-when-cross-origin), strictTransportSecurity (empty = not sent) |
CspContainer |
csp (directive => sources), integrations, useXContentSecurityPolicy (false); default policy: default/script/style/font/connect/child/media/object/manifest 'self', img 'self' data: |
UaCompatible |
browsers (IE=Edge), unsetOnStaticFiles (true), staticFilesExtensions |
PreventCookie |
maxAge (31536000), removeCookies, setCacheControl, setVaryHeaders, setConnectionHeaders, disableETags (all true); matches css js png jpg jpeg gif ico svg woff woff2 ttf eot files |
MimeTypes |
mimeTypes (['type' => '.ext'] or [['type' => β¦, 'extensions' => [β¦]]]; empty = common web types), includeCommonWeb, includeImages, includeDocuments |
ErrorDocuments |
errorDocuments ([404 => '/404.html'] or [['code' => 404, 'url' => β¦]]; default 404 β /error-404.html), commonErrorPages (base URL for 400β503 pages), customErrors |
FilesMatch |
pattern |
LimitExcept |
authMethods |
DeflateModule |
mimeTypes (text/html, text/css, application/javascript, application/json), browserCompatibility (true), fileExclusions ([]), varyHeader (true) |
ExpiresModule |
cacheRules ([['mimeType' => β¦, 'expiry' => 'access plus 1 month']]), defaultExpiry, useCommonRules (true β used when cacheRules is empty, with a 2-day default) |
BasicAuthModule |
authName (Protected Area), authUserFile, allowedPaths, allowedUserAgents, passwordComment, defaultPaths (true β favicon manifest files) |
NegociationModule |
multiViews (false), indexIgnore (*), customOptions |
ForceSecureRewrite |
excludePaths |
RedirectWwwRewrite |
β (['enabled' => true]) |
MaintenanceRewrite |
allowedIps, maintenanceFile (/maintenance.html), defaultState (false) |
RoutingRewrite |
baseUrl (/), checkTrailingSlash (true), versionedFiles (true), domainApps ([['domain' => β¦, 'file' => β¦]]), rules (raw strings or ['from', 'to', 'flags']), defaultController (index.php) |
SecurityRewrite |
blockUrlAttacks, blockSqlInjection, blockShellInjection, blockMaliciousRequests (all true), blacklistHttpMethods (HEAD TRACE TRACK OPTIONS PUT DELETE), blacklistUserAgents, blacklistReferrers, blockAction (/index.php), blockFlags (R=403,L) |
BrowserRender |
β (['enabled' => true]) |
API Reference
HtAccess Methods
$htaccess->withComments(bool $showComments = true): self $htaccess->withApacheCompatibility(bool $ensure = true): self $htaccess->addDirective(Directive|Container|string $directive): self $htaccess->toString(): string // Render a single directive or container on its own HtAccess::stringifyDirective(Directive|Container|string $directive, bool $showComments = true, bool $ensureApacheCompatibility = true, int $indent = 0): string
Container Methods
// All containers support $container->process(array $config = []): void $container->addDirective(Directive|Container|string $directive): self $container->ensureApacheCompatibility(): self $container->toString(bool $showComments = true, int $indent = 0): string
HtPasswd Methods
$htpasswd->addUser(string $name, string $password): self $htpasswd->toString(): string
Project Structure
src/
βββ HtAccess.php # Main .htaccess generator
βββ HtPasswd.php # .htpasswd generator
βββ Container.php # Base container class
βββ IfModule.php # <IfModule> container
βββ Directive.php # Base directive class
βββ EmptyLine.php
βββ Csp.php # Deprecated shim over jdz/cspmaker
βββ Container/ # Header, CSP, MIME, error, FilesMatchβ¦ containers
β βββ AntiXSS.php
β βββ CspContainer.php
β βββ ...
βββ Module/ # Apache module wrappers (IfModule)
β βββ DeflateModule.php
β βββ ExpiresModule.php
β βββ RewriteModule.php
β βββ ...
βββ Directive/ # Apache directive classes
βββ Header.php
βββ RewriteRule.php
βββ ...
Examples
See the examples/ directory for a complete, configuration-driven setup (it
uses symfony/yaml and jdz/data, both dev dependencies):
example.php- Generates a website, an API and a password-protected dev .htaccess intoexamples/exports/base.class.php-BaseHtAccess, which maps the YAML keys onto the containers and modulesmyhtaccess.php- The YAML config loader and thecreateHtAccessFromConfig()helperconfig/- YAML configuration layers (core,web,front,website,api,dev)
Changelog
-
1.1.4 -
process(['enabled' => false])renders nothing (an explicitly disabled module rendered its defaults); anEmptyLineinside a container renders as a blank line (it was dropped: the twoMaintenanceRewritesections were glued). -
1.1.3 -
SecurityRewrite::addRequestBlocking()renders patterns Apache can compile (the malformed-URI one had its closing parenthesis escaped: a 500 on every request) and matches..\traversal;addUserAgentBlocking()no longer blocks every crawler (its OR chain ended on(bot|crawler|spider|scraper)) and always closes the chain with its rule;RewriteModule::addMaintenanceMode()redirects off the maintenance page (R=301) instead of an internal rewrite. -
1.1.1 -
jdz/cspmakeris resolved from Packagist (no local path repository). -
1.1.0 - CSP building moved to
jdz/cspmaker(new dependency):CspContainerbuilds throughCspBuilderand acceptsintegrations;Cspbecomes a deprecated shim overJDZ\CspMaker\Policy. -
1.0.9 - Fixed malformed
Header/ExpiresModuleoutput that made Apache answer 500:Header::withVary()is a no-op,setCondition()emitsenv=β¦,PreventCookieappends realVaryheaders,ExpiresModuleis always wrapped in its<IfModule>. -
1.0.8 -
FilesMatch/LimitExceptshow comments by default.
License
This project is licensed under the MIT License. See the LICENSE file for details.
Contributing
- Fork the repository
- Create a feature branch
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
Author
Joffrey Demetz joffrey.demetz@gmail.com
Built with β€οΈ for secure and maintainable Apache configurations