- PHP 92.9%
- HTML 6.5%
- Makefile 0.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| spec | ||
| src | ||
| test | ||
| .gitattributes | ||
| .gitignore | ||
| composer.json | ||
| composer.lock | ||
| CONTRIBUTING.md | ||
| Makefile | ||
| phpcs.xml.dist | ||
| phpunit.xml.dist | ||
| README.md | ||
phly/microformats2
A PHP 8.5+ Microformats2 parser with Webmention classification support.
Why PHP 8.5?
- PHP 8.4 introduced
Dom\HtmlDocument, which includesquerySelector()andquerySelectorAll(), simplifying DOM traversal and detection of elements with specific attributes or CSS classes. - PHP 8.5 introduced the
Urinamespace and its URI parsing/representation implementations. This library usesUri\WhatWg\Urlfor validating, parsing, and manipulating URLs discovered in microformats.
Specifications
Specifications are pulled from the Microformats wiki and converted to markdown in ./spec/.
Installation
composer require phly/microformats2
Usage
Parsing Microformats
use Phly\Microformats2\Parser;
use Phly\Microformats2\MicroformatType;
use Phly\Microformats2\MicroformatPropertyType;
$parser = new Parser();
// Parse HTML with a base URL for resolving relative URLs
$result = $parser->parse($html, $sourceUrl);
// Access parsed microformat items
foreach ($result->items as $item) {
// $item is a Microformat object
echo $item->type[0]->value; // 'h-entry' (MicroformatType enum)
$item->type[0] === MicroformatType::HEntry; // true
echo $item->properties; // ['name' => ['My Post'], ...]
echo $item->id; // DOM element id, or null
echo $item->children; // Nested Microformat objects
echo $item->findParent(); // Parent Microformat, or null
}
// Access rel microformats
$authorUrls = $result->rels['author'] ?? [];
$meUrls = $result->rels['me'] ?? [];
// Serialize to JSON (mf2 JSON format)
$json = json_encode($result);
Working with Microformat Properties
Microformat properties are stored as arrays keyed by property name. The structure depends on the property type:
$entry = $result->items[0];
// p-* properties (text): array of strings
$name = $entry->properties['name'][0]; // 'My Post'
$note = $entry->properties['note'][0]; // 'A short note'
// u-* properties (URLs): array of strings (resolved URLs)
$url = $entry->properties['url'][0]; // 'https://example.com/post'
$photo = $entry->properties['photo'][0]; // 'https://example.com/photo.jpg'
// dt-* properties (dates/times): array of strings
$published = $entry->properties['published'][0]; // '2024-01-15T10:30:00Z'
// Nested microformats (e.g., p-author with an h-card)
$author = $entry->properties['author'][0];
if ($author instanceof \Phly\Microformats2\Microformat) {
echo $author->type[0]->value; // 'h-card'
echo $author->properties['name'][0]; // 'Jane Doe'
echo $author->properties['photo'][0]; // 'https://example.com/photo.jpg'
}
Property Types
The library provides a MicroformatPropertyType enum for type-safe property references. This enum covers all officially documented properties from the microformats2 wiki.
use Phly\Microformats2\MicroformatPropertyType;
// Look up a property type by its class name
$type = MicroformatPropertyType::tryFrom('p-name');
// Returns MicroformatPropertyType::PName
$type = MicroformatPropertyType::tryFrom('u-in-reply-to');
// Returns MicroformatPropertyType::UInReplyTo
$type = MicroformatPropertyType::tryFrom('myorg-p-name');
// Returns null (unknown/vendor-prefixed properties are not in the enum)
// Get the type prefix
MicroformatPropertyType::PName->prefix(); // 'p'
MicroformatPropertyType::UUrl->prefix(); // 'u'
MicroformatPropertyType::DTPublished->prefix(); // 'dt'
MicroformatPropertyType::EContent->prefix(); // 'e'
// Get the property name without the prefix
MicroformatPropertyType::PName->name(); // 'name'
MicroformatPropertyType::UInReplyTo->name(); // 'in-reply-to'
Querying Property Types
The Microformat object exposes methods to check property types:
$entry = $result->items[0];
// Get the property type for a specific property
$type = $entry->getPropertyType('name');
// Returns MicroformatPropertyType::PName
$type = $entry->getPropertyType('url');
// Returns MicroformatPropertyType::UUrl
// Check if properties of specific types exist
$hasText = $entry->hasPropertiesOfType([MicroformatPropertyType::PName]);
$hasUrl = $entry->hasPropertiesOfType([MicroformatPropertyType::UUrl]);
// Check by prefix
$hasPProperties = $entry->hasPropertiesWithPrefix(['p']); // true if any p-* exist
$hasEProperties = $entry->hasPropertiesWithPrefix(['e']); // true if any e-* exist
// Get the raw class name for a property
$rawClass = $entry->getRawPropertyClass('name'); // 'p-name'
Vendor-Prefixed Properties
Properties with vendor prefixes (e.g., myorg-p-name) are recognized by the parser but represented as MicroformatPropertyType::Extension:
// A vendor-prefixed property is parsed but uses the Extension type
$type = $entry->getPropertyType('name'); // MicroformatPropertyType::Extension
$raw = $entry->getRawPropertyClass('name'); // 'myorg-p-name'
Accessor Methods
The Microformat class provides convenient accessor methods for common property lookups:
$entry = $result->items[0];
// getProperty() — returns the first string value, or null
$name = $entry->getProperty('name'); // 'My Post'
$note = $entry->getProperty('note'); // 'A short note' or null
$miss = $entry->getProperty('nonexistent'); // null
// getProperties() — returns all string values as an array
$categories = $entry->getProperties('category'); // ['php', 'web', 'html']
// Content accessors (for e-content properties)
// getContent() and related methods access the 'content' property specifically.
// Note: getProperty('content') returns null — use these instead.
$htmlContent = $entry->getHtmlContent(); // Raw HTML from first e-content
$textContent = $entry->getTextContent(); // Plain text from first e-content
$allHtml = $entry->getAllHtmlContent(); // Array of all raw HTML values
$allText = $entry->getAllTextContent(); // Array of all plain text values
Accessing the DOM Element
Each Microformat provides access to its underlying DOM element:
$mf = $result->items[0];
// Get the DOM element
$element = $mf->getElement();
// Get the owning HTML document
$doc = $mf->getDocument();
// The element is a Dom\Element — use standard DOM methods
$element->getAttribute('class');
$element->textContent;
$element->innerHTML;
Webmention Classification
The library can classify webmentions by analyzing the source page's microformats relative to the target URL. This implements the response type detection from the IndieWeb.
Classifying a Webmention
use Phly\Microformats2\Parser;
use Phly\Microformats2\Webmention\Classifier;
$parser = new Parser();
// Parse the source page (the page that sent the webmention)
$result = $parser->parse($sourceHtml, $sourceUrl);
// Classify the webmention
$classifier = new Classifier(
result: $result,
sourceUrl: $sourceUrl, // URL of the page sending the mention
targetUrl: $targetUrl, // URL being mentioned
urlFetcher: fn(string $url): ?\Phly\Microformats2\Result => $this->fetchAndParse($url),
);
$webmention = $classifier->classify();
The $urlFetcher is a callable that fetches a URL and returns a parsed Result, or null on failure. It is used by the AuthorResolver to fetch external author pages (e.g., when the author is specified as a URL rather than an embedded h-card). If omitted, external author resolution is skipped.
Webmention Result Types
The classify() method returns one of five result types. All extend WebmentionResult which provides $source and $target properties.
Deletion
The source page no longer links to the target. The content has been deleted or removed.
use Phly\Microformats2\Webmention\Deletion;
/** @var Deletion $webmention */
$webmention->source; // 'https://source.example/post'
$webmention->target; // 'https://target.example/post'
Comment
The source page is a reply to the target. Detected via u-in-reply-to.
use Phly\Microformats2\Webmention\Comment;
/** @var Comment $webmention */
$webmention->source; // 'https://source.example/reply'
$webmention->target; // 'https://target.example/post'
$webmention->comment; // Webmention (h-entry) — the reply itself
$webmention->author; // Webmention (h-card) or null — the reply's author
// Access the comment content using accessor methods
$entry = $webmention->comment;
$name = $entry->getProperty('name'); // Reply title
$html = $entry->getHtmlContent(); // Reply HTML
$text = $entry->getTextContent(); // Reply plain text
// Access the author
if ($webmention->author !== null) {
$authorName = $webmention->author->getProperty('name');
$authorPhoto = $webmention->author->getProperty('photo');
$authorUrl = $webmention->author->getProperty('url');
}
Repost
The source page is a repost/boost of the target. Detected via u-repost-of.
use Phly\Microformats2\Webmention\Repost;
/** @var Repost $webmention */
$webmention->source; // 'https://source.example/repost'
$webmention->target; // 'https://target.example/post'
$webmention->author; // Webmention (h-card) or null — who reposted
if ($webmention->author !== null) {
$name = $webmention->author->getProperty('name');
$photo = $webmention->author->getProperty('photo');
}
Like
The source page is a like/favorite of the target. Detected via u-like-of.
use Phly\Microformats2\Webmention\Like;
/** @var Like $webmention */
$webmention->source; // 'https://source.example/like'
$webmention->target; // 'https://target.example/post'
$webmention->author; // Webmention (h-card) or null — who liked
if ($webmention->author !== null) {
$name = $webmention->author->getProperty('name');
$photo = $webmention->author->getProperty('photo');
}
Mention
A generic mention — the source page links to the target but does not have a specific response type.
use Phly\Microformats2\Webmention\Mention;
/** @var Mention $webmention */
$webmention->source; // 'https://source.example/mention'
$webmention->target; // 'https://target.example/post'
$webmention->title; // Page title string or null
$webmention->author; // Webmention (h-card) or null — page author
if ($webmention->author !== null) {
$name = $webmention->author->getProperty('name');
$photo = $webmention->author->getProperty('photo');
}
Serializable Webmentions
The Webmention class provides a DOM-free, serializable representation of microformat data. It mirrors the Microformat API but without getElement() or getDocument() methods, making it safe to store, cache, or transmit.
All properties are readonly and use constructor promotion.
use Phly\Microformats2\Webmention\Webmention;
// Convert a Microformat to a Webmention (recursively)
$webmention = Webmention::fromMicroformat($microformat);
// Access the same data as Microformat
$webmention->type; // list<MicroformatType>
$webmention->properties; // array<string, list<mixed>>
$webmention->id; // ?string
$webmention->children; // list<Webmention>
$webmention->parent; // ?Webmention
// Same accessor methods as Microformat
$name = $webmention->getProperty('name');
$html = $webmention->getHtmlContent();
$text = $webmention->getTextContent();
// Serialize to array (mf2 JSON format)
$array = $webmention->toArray();
// Reconstruct from serialized data
$restored = Webmention::fromJson($array);
Nested microformat objects in properties are automatically converted to Webmention instances during fromMicroformat(), and reconstructed from arrays during fromJson().
Author Resolution
The AuthorResolver implements the full authorship-spec algorithm. It resolves authors in this order:
- Embedded h-card — The entry's
authorproperty is an h-card microformat. - Author URL — The entry's
authorproperty is a URL; fetches the author page via$urlFetcherand looks for a representative h-card (matchingurl == uid == page URL, or matching arel=melink). - String name — The entry's
authorproperty is a plain text name; creates a minimal h-card. - Parent h-feed author — The entry's parent
h-feedhas anauthorproperty. - rel=author link — A
rel="author"link on the source page (backcompat). - Source page h-card — An h-card on the source page whose
urlmatches the author-page URL.
If no $urlFetcher is provided, steps requiring external fetches are skipped. If no author can be determined, null is returned.
Using Without a URL Fetcher
If you only need to classify based on inline markup (no external author-page fetching):
$classifier = new Classifier($result, $sourceUrl, $targetUrl);
$webmention = $classifier->classify();
// Author will be null if it requires fetching an external page
// Author will be resolved if it's embedded in the source HTML
Testing
php vendor/bin/phpunit