jturbide / form-to-email
A lightweight PHP 8.5+ library to process form submissions, validate fields, and send structured email notifications with configurable templates and error codes.
Requires
- php: >=8.5
- ext-intl: *
- ext-mbstring: *
- phpmailer/phpmailer: ^7.0.0
Requires (Dev)
- phpunit/phpunit: ^12.4
This package is auto-updated.
Last update: 2026-04-08 04:16:08 UTC
README
A lightweight, extensible PHP 8.5+ library for secure form processing, validation, sanitization, transformation, and structured email delivery. Built for modern PHP projects with strict typing, predictable pipelines, and framework-agnostic design.
π Quick start (tl;dr)
Install the package:
composer require jturbide/form-to-email
Create a form definition, create a mailer, and handle a request:
<?php use FormToEmail\Core\FieldDefinition; use FormToEmail\Core\FormDefinition; use FormToEmail\Enum\FieldRole; use FormToEmail\Filter\HtmlEscapeFilter; use FormToEmail\Filter\RemoveEmojiFilter; use FormToEmail\Filter\RemoveUrlFilter; use FormToEmail\Filter\SanitizeEmailFilter; use FormToEmail\Filter\StripTagsFilter; use FormToEmail\Filter\TrimFilter; use FormToEmail\Http\FormToEmailController; use FormToEmail\Mail\PHPMailerAdapter; use FormToEmail\Rule\EmailRule; use FormToEmail\Rule\IncludeRule; use FormToEmail\Rule\LengthRule; use FormToEmail\Rule\RequiredRule; use FormToEmail\Transformer\LowercaseTransformer; require_once __DIR__ . '/../vendor/autoload.php'; $form = new FormDefinition() ->add(new FieldDefinition('name', roles: [FieldRole::SenderName], processors: [ new TrimFilter(), new RequiredRule(), ])) ->add(new FieldDefinition('email', roles: [FieldRole::SenderEmail], processors: [ new SanitizeEmailFilter(), new EmailRule(), new LowercaseTransformer(), ])) ->add(new FieldDefinition('contact_reason', processors: [ new IncludeRule(['sales', 'support', 'billing']), ])) ->add(new FieldDefinition('number_of_units', roles: [FieldRole::Body], processors: [ new LengthRule(max: 10), ])) ->add(new FieldDefinition('message', roles: [FieldRole::Body], processors: [ new RemoveUrlFilter(), new RemoveEmojiFilter(), new StripTagsFilter(), new RequiredRule(), new HtmlEscapeFilter(), ])); $mailer = new PHPMailerAdapter( useSmtp: true, host: 'mail.example.com', username: 'no-reply@example.com', password: 'secret', fromEmail: 'no-reply@example.com', fromName: 'Website Contact Form' ); new FormToEmailController($form, $mailer, ['contact@example.com'])->handle();
Serve the file
php -s localhost:8000 ./
Send a XHR request
curl -X POST -H "Content-Type: application/json" -d '{ "name": "Julien", "email": "julien@example.com", "contact_reason": "support", "number_of_units": 12, "message": "Hello world!" }' http://localhost:8000/contact.php
See the result
{
"code": "ok"
}
π Recent updates
IncludeRulevalidates a field against a predefined allow-list and supports array inputs for checkbox and multi-select fields.- Default template rendering now safely handles mixed field values:
- numeric scalars are stringified before HTML/text rendering
- arrays and objects are JSON-encoded instead of causing rendering errors
- Error interpolation and template rendering share a single internal stringification helper for consistent output behavior.
- The project, docs, and CI now target PHP 8.5.
π Examples
Explore runnable examples in the examples/ directory. They cover both safe local exploration and real SMTP-backed delivery.
When an example uses handle() and real HTTP I/O, start the built-in server from the project root:
php -S localhost:8000
Examples list:
-
Basic example (safe local flow, includes logging)
- File:
examples/basic-example.php - Description: Uses
handleRequest()with an in-memory mailer so you can inspect the generatedMailPayloadwithout SMTP. - Run:
php examples/basic-example.php
- File:
-
SMTP basic (use
handle()and real HTTP I/O)- File:
examples/smtp-basic.php - Description: Minimal production-style endpoint using
PHPMailerAdapterover SMTP; replace host, credentials, and recipients with your own. - Run:
curl -sS -X POST -H 'Content-Type: application/json' \ -d '{"name":"Alice","email":"alice@example.com","message":"Hello!"}' \ http://localhost:8000/examples/smtp-basic.php | jq
- File:
-
Handle request without exit (capture response array)
- File:
examples/handle-request-json.php - Description: Demonstrates
FormToEmailController::handleRequest()by simulating$_SERVERand raw JSON; prints both a successful and a failing response. - Run (CLI, no web server needed):
php examples/handle-request-json.php
- File:
-
Advanced validators and processors
- File:
examples/advanced-validators.php - Description: Shows allow-lists, mixed-value fields, longer processor chains, and the structured validation payload returned on failure.
- Run (CLI):
php examples/advanced-validators.php
- File:
-
Custom JSON logging
- File:
examples/custom-logging.php - Description: Configures
Loggerto JSON format, writes toexamples/form.log, and demonstrates both validation-failure and success logging. - Run (CLI):
php examples/custom-logging.php
- File:
Tip: Examples are excluded from the Composer package and CI checks; they are for learning and local exploration.
β¨ Core Features
β Field Definitions & Validation
- Multiple validators per field (
RegexRule,CallbackRule,EmailRule, etc.) - Rich per-field error arrays for enhanced frontend UX
- Enum-based field roles (
SenderEmail,Subject,Body, etc.) - Composable and reusable rules (
RequiredRule,IncludeRule,LengthRule,RegexRule, etc.)
β Unified Processor Pipeline (Filters, Transformers, Validators)
- Single
FieldProcessorinterface powering all processing stages - Deterministic execution order: exactly the order you declare
- Configurable per-field pipeline with early bailout support
- Fully reusable across forms for consistent data behavior
β Filters & Sanitizers
- First-class sanitization layer with composable filters
- Advanced built-in filters:
SanitizeEmailFilterβ RFC 6531/5321 aware, IDN-safeRemoveEmojiFilterβ removes emoji and pictographic symbolsNormalizeNewlinesFilterβ consistent\nnormalizationHtmlEscapeFilterβ secure HTML output for templatesRemoveUrlFilterβ aggressively removes embedded URLsCallbackFilterβ supports custom callable filters
β Transformers
- Mutate values post-sanitization, pre-validation
- Includes built-ins:
LowercaseTransformer,CallbackTransformer, etc. - Ideal for formatting, slugifying, or normalizing input values
β Email Composition & Delivery
- Dual HTML and plain-text templates (fully customizable)
PHPMailerAdapterby default β pluggable architecture for other adapters- Enum-based structured responses (
ResponseCode::Success,ResponseCode::ValidationError, etc.) - Automatic field-role mapping to email metadata
- Safe rendering of numeric and mixed scalar field values in default email templates
β Logging & Observability
- Built-in
Loggerfor form submission tracking - Supports multiple formats: raw text, JSON, and syslog-compatible
- Optional toggles for successful or failed submission logging
- Fully tested with configurable verbosity and custom formatters
β Architecture & Quality
- Framework-agnostic β works with plain PHP, Symfony, Laravel, or any custom stack
- 100 % typed and static-analysis-clean (Psalm / PHPStan / Qodana / SonarQube)
- 445 PHPUnit tests with full coverage (core, filters, transformers, rules, adapters, logger)
- Predictable and deterministic pipeline ensuring consistent validation behavior
π§© Advanced Configuration
Built-in Adapters
MailerAdapter(interface) β minimal contract for sending mail.PHPMailerAdapterβ default adapter backed by PHPMailer.MailPayloadβ immutable value object that carriesto,subject,htmlBody,textBody,replyTo*.
Swap adapters by implementing
MailerAdapterand injecting your implementation intoFormToEmailController.
Built-in Filters
All filters implement Filter and the unified FieldProcessor contract. Use them to sanitize or normalize raw input.
TrimFilterβ trims leading/trailing whitespace.StripTagsFilterβ removes HTML tags.HtmlEscapeFilterβ escapes HTML for safe rendering.SanitizeTextFilterβ general text cleanup (safe subset).SanitizeEmailFilterβ RFC 5321/6531 aware, IDN-safe normalization.SanitizePhoneFilterβ digits-first cleanup for phone inputs.NormalizeNewlinesFilterβ converts mixed newlines to\n.RemoveUrlFilterβ removes URLs aggressively.RemoveEmojiFilterβ strips emoji and pictographs.CallbackFilterβ custom callable filter per field.- ...more to come!
Tip: Prefer filters early to make downstream validation predictable.
Built-in Rules (Validators)
Rules implement Rule (and FieldProcessor) and add ValidationError entries when constraints fail.
RequiredRuleβ value must be present/non-empty.IncludeRuleβ value must belong to an allowed set of developer-defined options.EmailRuleβ email syntax validation (works with IDN-normalized values).RegexRuleβ arbitrary pattern checks.LengthRule/MinLengthRule/MaxLengthRuleβ string length constraints.CallbackRuleβ custom boolean/callback validation.- ...more to come!
Errors use
ErrorDefinitionwith machine-readablecode,message, andcontext.
Example: allowed values
use FormToEmail\Rule\IncludeRule; $field = new FieldDefinition('contact_reason', processors: [ new IncludeRule(['sales', 'support', 'billing']), ]);
Example: multi-select values
$field = new FieldDefinition('topics', processors: [ new IncludeRule(['news', 'events', 'offers']), ]);
If the frontend sends ['news', 'offers'], the rule passes. If it sends ['news', 'unknown'], the rule adds a structured invalid_value error with the rejected entries in the error context.
Built-in Transformers
Transformers modify values after sanitization but generally before rules.
LowercaseTransformerβ lowercases strings.UcFirstTransformerβ capitalizes first letterCallbackTransformerβ custom mapping logic.- ...more to come!
Keep formatting here (slugify, case changes, phone canonicalization) so rules validate the final shape.
Processor Order
Execution is exactly the order you define in each FieldDefinition. A good default convention is:
Filters β Rules (Validators) β Transformers
Explicit order example
$field = new FieldDefinition('username', roles: [], processors: [ new HtmlEscapeFilter(), new TrimFilter(), new LowercaseTransformer(), new RegexRule('/^[a-z0-9_]{3,20}$/'), ]);
Incremental API
$field = new FieldDefinition('username'); $field->addFilter(new HtmlEscapeFilter()); $field->addFilter(new TrimFilter()); $field->addProcessor(new LowercaseTransformer()); $field->addRule(new RegexRule('/^[a-z0-9_]{3,20}$/'));
Chaining
$field = (new FieldDefinition('username')) ->addFilter(new HtmlEscapeFilter()) ->addFilter(new TrimFilter()) ->addProcessor(new LowercaseTransformer()) ->addRule(new RegexRule('/^[a-z0-9_]{3,20}$/'));
The library does not reorder processors for you. Define the pipeline you want. As a rule of thumb: sanitize first, validate, then transform to final form.
Value Types And Rendering
The validation pipeline accepts mixed values because frontend payloads are not always strings.
Practical implications:
- Filters that operate on text should usually pass non-string values through unchanged.
- Rules should decide explicitly whether non-string values are valid, ignored, or rejected.
- Default HTML and plain-text email templates safely stringify numeric scalars and JSON-encode arrays/objects.
That means payloads like:
{
"numberOfUnits": 12,
"topics": ["news", "offers"]
}
will render safely in generated emails without requiring you to cast everything to strings first.
Custom Processors (Advanced)
Implement the unified processor contract to add your own behavior:
use FormToEmail\Core\{FieldProcessor, FieldDefinition, FormContext}; final class SlugifyTransformer implements FieldProcessor { public function process(mixed $value, FieldDefinition $field, FormContext $context): mixed { $s = strtolower(trim((string)$value)); $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? ''; return trim($s, '-'); } }
Register it like any other processor:
$field = (new FieldDefinition('title')) ->addProcessor(new SlugifyTransformer());
You can also stack your own custom ones:
$field->addProcessor(new CallbackTransformer( static fn(mixed $v): mixed => is_string($v) ? preg_replace('/\s+/', ' ', $v) : $v ));
π§ Future Roadmap
This library already covers the essentials for form validation, sanitization, transformation, and email delivery. However, thereβs still plenty of room for evolution (as always). While most of the following items arenβt priorities for my own use cases, Iβm open to implementing them if theyβre valuable to you or your project.
β Completed
- Sanitization filters β
- Data transformers β
- Unified processor pipeline β
- Comprehensive unit test coverage β
- Submission logging system β
π§ Planned / Proposed
- reCAPTCHA v3 integration β prevent bot submissions without user friction
- File attachments β safely handle uploaded files via configurable limits
- Sender confirmation & double opt-in β ensure sender authenticity before sending
- Spam protection / honeypot β lightweight anti-spam defense
- Webhook + API notifications β trigger external systems on successful submissions
- Rate limiting & IP throttling β basic abuse protection for public endpoints
- Additional mailer adapters β e.g. Symfony Mailer, AWS SES, Postmark
- SmartEmailRule β enhanced email validation with MX/DNS deliverability checks
π‘ Want a feature sooner?
Open a GitHub issue or start a discussion β contributions and ideas are always welcome!
π§ͺ Quality Assurance
This library is built for reliability, maintainability, and modern PHP ecosystems.
- 100 % strictly typed β every file uses
declare(strict_types=1) - 100 % code coverage β verified through both unit and integration tests
- Modern PHP 8.5 syntax β strict typing, readonly constructs, attributes, and enhanced type safety
- Continuous Integration β fully validated via GitHub Actions with PHPUnit, Psalm, and PHPStan
- Comprehensive test coverage β 445 PHPUnit tests covering core, filters, transformers, rules, HTTP flow, templates, and mail adapters
- Deterministic pipeline β predictable processor order with verified behavior across all test cases
πͺͺ License
BSD 3-Clause License Β© Julien Turbide