Search by

rlmumford / document

rlmumford

A document: one file people treat as a unit, what it is, what state it is in, and what has been worked out about it.

Package info

github.com/rlmumford/document

Type:drupal-module

pkg:composer/rlmumford/document

Statistics

Installs: 309

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

2.x-dev 2026-09-15 14:12 UTC

This package is auto-updated.

Last update: 2026-09-15 14:12:33 UTC


README

A document: one file people treat as a unit, what it is, what state it is in, and what has been worked out about it.

file and files

Files arrive in files, as they were given to us. Where several make up one document — six photographs of a bank statement, a covering letter and a CV — they are composed into file, which is the single readable thing anyone actually opens.

The originals stay. A composition can be wrong, a page can be missing, and the only way to find that out afterwards is to still have what came in.

Status

needed, not_needed, received, refused, approved, rejected, expired, superseded.

needed is the one that justifies the entity: a document somebody has been asked for and has not sent is still a thing the system must hold an opinion about, and a file cannot represent the absence of itself. superseded is the other — replacing a document must not mean destroying the one it replaced.

A plain list rather than a workflow, because the transitions differ by document type and by whose document it is, and baking one set of them in decides that for everybody.

analysis_data

Whatever has been worked out about the document — extracted text, a parse, a model's answer — with what produced it. It belongs with the document rather than in a cache that can be cleared.

A map, so it is an array in PHP and serialized in storage, which means it is not queryable. CounselKit's D7 original used a real MySQL JSON column and can query it. When something here needs that, the column type is the change rather than the shape of the data.

Types

A config bundle, because a CV and a bank statement share nothing but a file field. A general type ships so the module is usable on install; add your own before it accumulates everything.

Choosing a document instead of uploading one again

document_selector is a widget for an entity_reference field pointing at documents. It offers the documents this person has already given us, of the right type, alongside an upload for a new one — and asks what to call a new one, so next time the list is a choice rather than three files called document.pdf.

Configure it with the document type new uploads become, and the extensions and size you will accept.

It serves both entity_reference and entity_reference_revisions. On a revision field it records which version of the document was chosen, so replacing a file later does not rewrite what was already sent somewhere.

owner and person

The owner is whoever provided the document and is answerable for it. person is whose life it describes. Usually the same; occasionally the whole point — a case worker scanning a client's bank statement, a recruiter uploading somebody else's CV.

Named for what it holds rather than for the relationship. A document can be about a bank account, a property, a company; "about" would have to mean all of them and so would mean nothing, and one field cannot hold them anyway without dynamic_entity_reference. Those get fields of their own when something needs them.

getPersonId() reads person, falling back to the owner where nobody said otherwise, and document_selector offers documents on the same basis. That fallback is why person is left empty rather than defaulted: "nobody said" has to stay distinguishable from "it is theirs", or the recruiter case silently becomes the recruiter's own document.

What this module deliberately does not know

Whether a document contains special-category data under Article 9. That question only means something alongside a consent model — what you are allowed to do having been told yes — and a module that stores files should not carry half of one. Add the field where the consent lives.

Revisions

Replacing the file in a document creates a new revision rather than overwriting it, so anything that referenced the old one — an application that was already sent, say — still resolves to what it actually sent.

Revisions carry a log: who made them, when, and why. Without that, "there are four versions of this" answers nothing, and the question asked of a document years later is always who changed it and what they were doing.

The log is deliberately not inherited. revision_log is a revisionable field like any other, so loading a document and asking for a new revision would carry the previous message forward untouched - and every revision would then claim the reason given for the first one that had a reason. That is worse than an empty log: an empty log says nobody recorded why, an inherited one says something false.

So setRevisionLogMessage() stages a message rather than writing it, and preSave() puts whatever was staged onto the revision being written - nothing, if nothing was staged. The field itself goes on saying what was actually stored, so loading a document and reading its log gives the reason the current revision was made, which is the only thing anybody wants from it. getPendingRevisionLogMessage() reads what is staged, if you need it.

Who and when are stamped on every new revision, not only the ones made through a form - otherwise a revision created by an update hook or a migration has no author and no date.