phil / listings
The core of listings — classified ads, job offers, searches for partners: bounded kinds, typed criteria that meet a search, an area shown to all and an exact place shown to accepted respondents, expiry with reminders and extensions, moderation before or after publication, and responses that put two
Requires
- php: >=8.5
- ext-mbstring: *
- psr/clock: ^1.0
Requires (Dev)
- phpunit/phpunit: ^13.4
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-11-11 13:30:55 UTC
README
The core of listings: a classified ad, a job offer, a search for partners — written, moderated, answered and expired by the same rules.
phil/listings is the part every kind of listing shares. A listing has a title, typed criteria a search can meet, an area shown to everyone with an exact place shown only to the people its author accepts, and a minimum age. It is published at once or after a moderator's approval, hidden by enough reports, reminded before it expires, extended a bounded number of times and closed by its author. Members respond to it; a response carries no message, and only a response the author accepts lets the application put the two of them in touch, with the consent of each.
It is for any PHP application where members post offers or requests to one another: small classified ads between
members, the job offers of a community, people looking for partners for an activity, a marketplace between members
and sellers. Each of those is a kind of listing with its own KindPolicy — lifetime, reminder, extensions,
moderation, reports, minimum age, open responses — and its own fields and screens, built on this core.
It is a plain PHP library whose requirements are ext-mbstring and psr/clock. Listings and
responses are immutable: each transition takes the policy of the listing's kind and an instant, and returns a new
value, so the whole life of a listing is tested to the second and stored as plain values. What the library cannot
know is a port: who blocked whom (BlockList), how old a member is (MemberAges) and where the responses live
(ResponseStore). Every value is bounded — titles, labels, criteria, choices, days, reports, responses — so that
nothing a member types or a configuration says can make a listing unbounded.
Status
| Part | State |
|---|---|
| Identifiers, kinds, moderation reasons, titles, places | Implemented and tested |
| Criteria — choices, integer ranges, flags — and their meeting with a search | Implemented and tested |
| Kind policies, bounded, and their registry | Implemented and tested |
| The life of a listing: draft, submission, moderation before or after publication, reports, edits, expiry, reminders, extensions, closing, removal | Implemented and tested |
Responses — pending, accepted, declined, withdrawn — and the Responder rules: visibility, own listing, blocks, minimum age, one open response per member, open responses per listing |
Implemented and tested |
| Persistence, forms, screens, notifications, conversations, payment | Not in this library: they belong to the application or to a bundle built on it |
Documentation
| Document | What it covers |
|---|---|
| Installation | Requirements, installing, declaring the kinds. |
| Listings | Titles, places, the minimum age, the statuses and every transition. |
| Criteria | Choices, ranges and flags, and how a search meets a listing. |
| Moderation and expiry | Moderation before or after publication, reports, expiry, reminders and extensions. |
| Responses | Responding, accepting, declining, withdrawing, and the rules of the Responder. |
| Extension points | The block list, the members' ages and the store of responses. |
| Exceptions | ListingsException and its exceptions. |
| Testing | The library's tests, mutation bar and benchmark, and how to test code built on it. |
| Reference | Every type of the library, with its role. |
Quick start
composer require phil/listings
use Phil\Listings\Criteria; use Phil\Listings\Criterion; use Phil\Listings\KindPolicies; use Phil\Listings\KindPolicy; use Phil\Listings\Listing; use Phil\Listings\ListingId; use Phil\Listings\ListingKind; use Phil\Listings\Location; use Phil\Listings\MemberId; use Phil\Listings\Responder; use Phil\Listings\ResponseId; use Phil\Listings\Title; $partners = KindPolicy::of(ListingKind::of('partner'), lifetimeDays: 30, minimumAge: 13); $policies = new KindPolicies([$partners]); $listing = Listing::draft( ListingId::of('01JC4Z9X8Y7W6V5T4S3R2Q1P0N'), $partners, MemberId::of('member-42'), Title::of('Two players wanted for a weekly campaign'), Criteria::of([Criterion::choice('day', ['sat']), Criterion::range('players', 2, 2)]), Location::area('Plateau', '4200 Saint-Laurent'), 18, $clock->now(), )->submit($partners, $clock->now()); // published at once: the kind is moderated after publication $responder = new Responder($policies, $blockList, $memberAges, $responseStore, $clock); $response = $responder->respond($listing, ResponseId::of('01JC5A…'), MemberId::of('member-7')); $accepted = $responder->accept($listing, $response); // both consent: the application may open a conversation
$blockList, $memberAges and $responseStore are the application's: see Extension points.
Requirements
PHP 8.5 or later, ext-mbstring and psr/clock.
Contributing
Contributions are welcome on the PHIL monorepo: read the contribution guide and the code of conduct. Report a vulnerability privately, as the security policy explains, never in a public issue.
License
MIT, © Philippe Gamache, doing business as Le Gnome Archiviste; a Parler Haut Interagir Librement project. See LICENSE.