Aller au contenu

omnibase/forge Référence de l’API

Étage
Structure préparée · Omnibase
Version
main
Vérifié en réel
Son README ne le dit pas
Licence
MIT
composer require omnibase/forge:dev-main

Lue dans les fichiers PHP du paquet au commit 200df927930c : ses espaces de noms, classes, interfaces et énumérations, leurs constantes, propriétés et méthodes publiques avec leur PHPDoc, et les options que prennent ses fabriques. Rien n’en est exécuté.

Base\Forge

  • ForgeBundle class

    The studio's forge on top of base-bundle-market: software and its releases (built from git tags through git/git-bundle), free and licensed downloads, a private Composer repository, client projects with a shared board and logged time, prepaid support hours and quotes.

    3 methods

Base\Forge\Command

  • ArtifactAttachCommand class

    A build made elsewhere (Unity, a CI) becomes a release's artifact for a platform - the release made when it does not exist yet (a game need not come from a repository's tags):

    1 method

  • HoursLowBalanceCommand class

    Tells the clients whose support hours run low (under forge.low_balance_minutes, or overdrawn) - weekly, by cron.

    1 method

  • LicenseExpireCommand class

    Marks the licences past their expiry as expired. They already stopped working at that instant (License::isValid() reads the date); this keeps the status honest for the back office and the account page.

    1 method

  • LicenseKeypairCommand class

    A new Ed25519 pair for the activation tokens: the secret goes in the vault (FORGE_LICENSE_SIGNING_KEY), the public key in the software. A new pair makes every token issued so far unverifiable: the software asks again, online.

    1 method

  • ReleaseSyncCommand class

    New tags become releases: fetch each software's repository (when it has a url), then create a release and its zip for every version tag not seen yet. Run hourly by cron; `make releases` runs it by hand.

    1 method

Base\Forge\Controller\Admin

  • ActionController class

    The back office's buttons that are not a form: rebuild a release's archive, send a quote, ask a client for their review of a delivered project, and mark an order paid when its bank transfer arrives (the manual gateway leaves it pending; confirming it here is what credits the hours and issues the licences).

    5 methods

  • BuildController class

    A release's builds, one per platform (a game's, an application's): each with its size and its sha256, and a form to upload one - it replaces the platform's previous build (ArtifactBuilder::attach()). What forge:artifact:attach does from a CI.

    2 methods

  • PipelineController class

    The pipelines in the back office: the latest runs of every project, and one of them as its graph - its jobs by stage, in the colour of their status (@glitchr/graphjs, which the back office's layout loads).

    3 methods

  • RefundController class

    Refunding hours a client bought (HourRefunds): what the order paid and what is left of its hours, an amount and the hours to take back - suggested from what is unused -, and the explanation the client receives. Nothing is sent without one.

    2 methods

Base\Forge\Controller\Admin\Crud

  • CardCrudController class

    The cards of the project boards (the clients move them from their dashboard).

    3 methods

  • DownloadCrudController class

    Who downloaded what, when, through which licence and which way (the site or Composer): the log the downloads write themselves - to read, not to edit.

    4 methods

  • HourCreditCrudController class

    The support-hour ledgers, read-only: lines come from paid orders and logged time; a purchase has a "Rembourser" action. A manual correction is an adjustment line from the forge_admin_hours_adjust action (to keep the history honest).

    4 methods

  • HourPackCrudController class

    Hour packs: a market product (store, price, stock) plus the hours it credits.

    3 methods

  • HourRefundCrudController class

    The refunds made, read-only: they are made from a purchase (RefundController).

    4 methods

  • LicenseActivationCrudController class

    The machines the licensed software is activated on, seat by seat: made by the software itself (the licence API), freed here by deleting - its token runs out, and it must ask again. To give more places, raise the licence's machines per seat.

    4 methods

  • LicenseCrudController class

    Licences: issued when an offer is paid; granted by hand here (a gift, a partner, a client's team) - then give its seats, an e-mail each, under "Postes". Keys are never shown: each seat's holder makes theirs from their account.

    3 methods

  • LicenseOfferCrudController class

    Licence offers: a market product that issues a licence of its software per unit paid.

    3 methods

  • LicenseSeatCrudController class

    The seats of the licences: who may use each - an e-mail. Saving one tells its holder by e-mail (LicenseSeatListener); whoever signs in with that address, verified, downloads and makes their key. Deleting it takes the access back at once. Not more seats than the licence has: raise its number first.

    3 methods

  • PipelineCrudController class

    The pipelines recorded: which project, which commit, where they ran - and each as its graph.

    4 methods

  • ProjectCrudController class

    Client projects: who, which repository, how many hours agreed.

    4 methods

  • ProjectReviewCrudController class

    The clients' reviews of their delivered projects: asked from the projects ("Demander un avis"), given by the client. Tick "Affiché" to show one on the site; the signature can be shortened, the words are the client's.

    4 methods

  • QuoteCrudController class

    Quotes and special offers: requests arrive from the site; price the lines, set a discount and a date, then "Send" mails the client their link.

    6 methods

  • ReleaseCrudController class

    Releases: made from tags by forge:release:sync, or by hand; "Build" (re)makes the zip (and a Unity package's tarball); "Builds" lists and uploads a game's or an application's builds, one per platform.

    4 methods

  • SoftwareCrudController class

    The software (base-bundle threads: translated title, tagline and description; published, they are on the applications page).

    3 methods

  • TimeEntryCrudController class

    Time spent on client projects. Every billable entry comes off the client's support hours in the same save (TimeEntryLedgerSubscriber).

    4 methods

Base\Forge\Controller\Client

  • AccountController class

    The client's side of the forge: their licences, the seats they give and the keys of the ones they hold, how to install from the Composer repository, their support hours, their quotes and what they downloaded.

    6 methods

  • BoardController class

    A project's board: the page that shows it, and the move of a card - what its script (public/js/board.js) sends at each drop. Who sees a project and who moves its cards is ProjectVoter's: its client and the staff; a delivered project no longer moves for its client.

    3 methods · 1 constant

  • ComposerController class

    The Composer repository's single document. Anonymous: the free packages.

    2 methods

  • DownloadController class

    The software page and its downloads. A download is two steps: the version link checks who you are against the licence (ArtifactVoter) and redirects to a signed, short-lived file URL; that URL is served by nginx (X-Accel-Redirect to forge.accel_prefix) or, without it, by PHP.

    5 methods

  • HubController class

    What the hub clients read beside the Unity registry (docs/hub.md): the hub manifest, the catalogue - its paid packages too, with their offers, and the games -, and the device flow that links an editor to its user's licences once bought (DeviceLinks). Under /upm, whose firewall the host leaves open: the licence key comes as the registry's Bearer token, the device code in the body.

    6 methods

  • HubLinkController class

    The page a hub client sends its user to (the device flow, DeviceLinks): signed in, they see the code their editor shows, the item it asked for with its offers - bought here, in a tab of their own, through the shop's cart and checkout - and, once they hold a licence, "Link this editor": the editor then receives a key of theirs and installs.

    2 methods

  • LicenseApiController class

    The licence API the studio's software calls (a Unity game, a desktop application): activate a machine and get a signed token to run offline; free the machine; the public key to check the tokens with. JSON, no session: the seat's key is the credential. See docs/licensing.md.

    4 methods

  • PlayController class

    A game's WebGL build, played in its page (the software page frames /play/<slug>/<version>/ once the visitor presses Play): the build's own index.html, then its loader and its compressed files, with the headers WebglBuilds gives them. A free game is played in the browser by anyone; a paid one is downloaded once bought, and played here only when the studio opened a playable preview of it (Software::$playablePreview) - then by anyone too. The staff always, to check a build.

    2 methods

  • ReviewController class

    Where the client's review link lands: their project, delivered and closed, and the form - one to five stars, a few words. Their link is the credential: no account needed. Answered once; then it thanks them.

    1 method

  • ShopController class

    Support, sold by the hour: the packs of the support store, the special offers (quotes) made to the signed-in client, and the form asking for one.

    5 methods

  • UpmController class

    The Unity registry (UpmRegistry), as Unity's Package Manager reads a scoped registry: the search, the package documents, the tarballs.

    5 methods

Base\Forge\DependencyInjection

Base\Forge\Entity

  • Artifact class

    A file a release is downloaded as, stored under forge.storage_dir (never in the web root) and handed out by DownloadController. Its format says what it is: "zip", the tree at the tag (ArtifactBuilder); "upm", the same tree as the npm tarball Unity's Package Manager installs (UpmRegistry); or a platform's value (Platform: webgl, windows, macos, linux, android), the build of a game or an application for it.

    18 methods

  • Card class

    A task on a project's board. The client and the studio both move them.

    17 methods

  • Download class

    One artifact handed out: who, under which licence, when. The IP is kept hashed.

    7 methods

  • HourCredit class

    One line of a client's support-hour ledger: positive when hours are bought, negative when time is logged. The balance is their sum; nothing else stores it, so it can never drift from its history.

    13 methods

  • HourRefund class

    Hours bought, paid back: the studio keeps the right to refund a purchase - when the work asked for does not sit with the client's values, or the studio's - and says why. The amount goes back on the card through Stripe (or by bank transfer, by hand, for the manual gateway); the hours it covered leave the ledger (a `refund` line); the client is told, with the explanation.

    16 methods · 2 constants

  • Job class

    One thing a stage runs: a test suite, a build, a deployment.

    21 methods

  • License class

    The right to download a licensed piece of software - and to install it from the Composer repository. Granted when a LicenseOffer is paid for, or by hand from the back office.

    29 methods

  • LicenseActivation class

    One machine a seat's software is activated on: a game, an application installed on a computer. The licence allows so many machines a seat (License::$machinesPerSeat); whoever holds the seat frees one from their account - a new computer, an old one gone - and the studio from the back office.

    16 methods

  • LicenseSeat class

    One seat of a licence: the e-mail it is given to. Whoever signs in with that e-mail - verified - downloads what the licence covers and installs it with Composer, with a key of their own. The licence's owner gives its seats, and takes them back; a paid licence starts with the owner's.

    23 methods

  • Pipeline class

    One run of a project's checks on a commit - its tests, its build, its deployment -, wherever it ran (a forge's CI, a script): its stages in order, each with its jobs. The forge keeps what happened and shows it (a graph, drawn by @glitchr/graphjs); it runs nothing itself.

    25 methods

  • Project class

    A client's project: what the dashboard shows them - the board, the time the studio logged on it, and the history of its git repository. A base-bundle thread: its title and excerpt are translated, its client is also its owner (base-bundle's own notion of whose a thread is).

    25 methods

  • ProjectReview class

    What the client thinks of a delivered project. The studio asks for it from the back office (the project is then closed: delivered, its board no longer moves); the client answers through their link - one to five stars, a few words -; the studio decides whether it shows on the site.

    17 methods

  • Quote class

    A special offer made to one client: lines of hours at a rate, a discount, a date it holds until. Asked for from the quote form, priced in the back office, read and accepted by the client - which turns it into a one-off HourPack in their cart (Forge\Service\QuoteToOrder); paying that order credits the hours and marks the quote paid.

    11 methods

  • QuoteLine class

    One line of a quote: a piece of work, its hours, the rate.

    16 methods

  • Release class

    One version of a piece of software: a git tag, the commit it points at, the tag's message as changelog, and the archives built from that tree.

    20 methods

  • Software class

    Something the studio made and shows: a client site, a bundle, a JavaScript library, a tool. A base-bundle thread, as market's stores and products are: its title, headline and content are translated (FR/EN), its slug and state come with it - published, it is on the applications page. With a git repository it gets releases; FREE ones download for anyone, LICENSED ones against a licence bought through one of its LicenseOffer products. Its kind says how it is delivered: a package (Composer, Unity's Package Manager), a game (played in the page, downloaded per platform) or an application.

    64 methods · 1 constant

  • Stage class

    A step of a pipeline ("test", "build", "deploy"): the jobs that run together, after the stage before.

    12 methods

  • TimeEntry class

    Time the studio spent on a project. Logging it takes the same minutes off the client's support hours (Forge\Service\HourLedger, through TimeEntrySubscriber), so the balance on their dashboard is always the sum of what they bought and what was done.

    18 methods

Base\Forge\Entity\Product

  • HourPack class

    Support hours, sold like any product of base-bundle-market: paying for n of them credits n × minutes to the buyer's ledger (OrderPaidSubscriber).

    9 methods

  • LicenseOffer class

    A licence for a piece of software, sold through base-bundle-market: paying for it issues a Forge\Entity\License per unit (OrderPaidSubscriber).

    11 methods

Base\Forge\Enum

  • CardColumn enum

    The four columns of a project's board, in the order they are shown.

    4 cases

  • CreditReason enum

    Why a client's support-hour balance moved.

    4 cases

  • LicenseStatus enum

    3 cases

  • PipelineStatus enum

    Where a pipeline, or one of its jobs, stands. The values are the states @glitchr/graphjs draws (its `pipeline` preset): a status is given to a node as it is.

    2 methods · 8 cases

  • Platform enum

    A platform a game or an application is built for. Its build is a release artifact whose format is the platform's value: a zip of the build folder (webgl, windows, macos, linux) or the package itself (android: an .apk or an .aab).

    4 methods · 5 cases

  • Pricing enum

    Whether a piece of software is downloaded freely or against a licence.

    1 method · 2 cases

  • ProjectStatus enum

    3 cases

  • SoftwareKind enum

    What a piece of software is delivered as: a package a project installs (Composer, Unity's Package Manager), a game people play (in the page when it has a WebGL build, else downloaded per platform), or an application people install.

    3 cases

  • SoftwareStatus enum

    3 cases

Base\Forge\EventListener

  • LicenseSeatListener class

    A seat saved from the back office tells its holder, as one given from the owner's account does (LicenseSeats::give, which sends it itself: the `notified` flag keeps it to one e-mail).

    2 methods

Base\Forge\EventSubscriber

  • LicenseApiErrorSubscriber class

    The licence API answers its errors in JSON, with their own status - a request it cannot read is a 422 with what is wrong -, never the site's HTML error page: the one reading it is a game or an application.

    2 methods

  • OrderPaidSubscriber class

    What a paid order delivers, since nothing here ships:

    2 methods

  • TimeEntryLedgerSubscriber class

    Keeps every time entry's debit line in step with it, whoever writes the entry (the back office, a command, a fixture): logged, changed or made non-billable, the client's balance follows in the same flush. A deleted entry takes its line with it (ON DELETE CASCADE).

    2 methods

Base\Forge\Exception

  • HourRefundException class

    A refund HourRefunds would not make, or Stripe refused; nothing was changed.

  • LicenseActivationException class

    Why an activation is refused: a reason the software can act on, and its HTTP status.

    2 methods · 2 properties · 5 constants

Base\Forge\Form

  • QuoteLineType class

    One line of a quote in the back office: the work, its hours, the rate per hour (cents).

    3 methods

  • ReviewAnswerType class

    The review form: five stars (radios, drawn as stars), a few words, how to sign. The texts name their domain (@forge.…): base-bundle gives every field the "fields" domain.

    2 methods

Base\Forge\Git

  • ForgeRepositoryProvider class

    Hands git/git-bundle the repositories of the projects and the software, so a new client project is browsable (and clonable by git:sync) the moment it is saved - no git.repositories line to add. Each lives at forge.repositories_dir/<repository>.git; one also declared in git.repositories keeps its configured path.

    2 methods

Base\Forge\Model

  • ActivationRequest class

    What a piece of software sends to activate - or re-activate - its machine.

    1 method · 6 properties

  • CardMove class

    A drop on a project's board: which column, at which position (BoardController, #[MapRequestPayload]).

    3 methods · 2 properties

  • ReleaseRequest class

    What a piece of software sends to free its own machine's place.

    1 method · 2 properties

  • ReviewAnswer class

    What the client answers about their project.

    3 properties

Base\Forge\Repository

Base\Forge\Security\Voter

  • ArtifactVoter class

    Who may download an artifact: anyone when its software is free; else the holder of a valid licence covering its release - and the staff.

    1 method · 1 constant

  • GitRepositoryVoter class

    git/git-bundle's per-repository check (git.repository_attribute: GIT_VIEW): the staff browse everything; a client their projects' repositories; the source of a free piece of software is open to any signed-in visitor, that of a licensed one to its licence holders. A repository nothing here claims (declared only in git.repositories) stays the staff's.

    1 method · 1 constant

  • ProjectVoter class

    A project is its client's, and the studio's: the client sees it and moves its cards - until it is delivered (closed); staff see and move every one.

    1 method · 2 constants

Base\Forge\Service

  • ArtifactBuilder class

    Builds a release's zip straight from its git tag - every file of the tree, read by git/git-bundle from the object database, under a <software>-<version>/ folder. Files marked export-ignore are not filtered (there is no .gitattributes evaluation here): keep the tree clean.

    8 methods

  • ArtifactFiles class

    An artifact's file handed out, once whoever asks was let through: one Download row for each new download (not a HEAD, not the rest of a download resumed), then the file - by nginx (X-Accel-Redirect to forge.accel_prefix) or, without it, by PHP. The signed links (DownloadController) and the Unity registry's tarballs (UpmController) both end here.

    2 methods

  • Board class

    A project's board, as it is moved: a card put at a place of a column, and the columns it touched renumbered from zero - so the positions always read 0, 1, 2... with no gap and no tie, whatever was dragged where. Nothing is saved here: the caller flushes.

    2 methods

  • ComposerIndex class

    The studio's own Packagist: the index (packages.json) of a Composer repository of the PHP packages among the software. Free ones are listed for anyone; licensed ones for the holder of a valid licence, each version its licence covers, with a dist URL signed for that licence.

    3 methods

  • DeviceLinks class

    An editor linked to its user's licences, by a device code (as OAuth's device flow): the Unity window asks for a code, the user opens the link page on the site, signs in, buys what they need, and links the editor; the window, which asked meanwhile, then receives a licence key - the key of one of the user's seats, made anew (the seat's previous key stops working) - and stores it as the registry's token in the user's .upmconfig.toml.

    6 methods · 2 constants

  • HourLedger class

    A client's support hours: credited when they buy some, debited when time is logged on their projects. The balance is always the sum of the lines.

    5 methods

  • HourRefunds class

    Paying hours back. The studio keeps the right to refund hours a client bought - when the work does not sit with their values, or the studio's - with an explanation, always. What is refundable is what the order paid, less what was already refunded; what it suggests is the share of the hours still unused (the ledger is one pool: the order's hours, capped by the balance left).

    5 methods

  • HubCatalog class

    What the hub clients read - the Unity window (com.glitchr.hub) and the desktop hub: the hub manifest (/upm/-/hub.json: the brand, the sources, whether and how to buy) and this forge's catalogue (/upm/-/hub/catalog.json: its Unity packages, free and paid, with their versions, changelogs, pictures and offers, and its games). The Unity registry itself (UpmRegistry) stays what Unity's Package Manager reads; the catalogue adds what a shop window shows.

    4 methods

  • LicenseActivations class

    The machines a seat's software runs on. The software sends its seat's key and its machine's identifier: the first time, the machine takes one of the seat's places (License::$machinesPerSeat); every time, it gets a fresh signed token (LicenseToken) to run offline until the next. When every place is taken it is told which machines hold them - its holder frees one from their account, or from that machine (release()).

    4 methods

  • LicenseHolders class

    Who is behind a licence key a package manager sends: the seat that key was made for, on a valid licence - and then every valid licence with a seat for that seat's e-mail. Composer sends the e-mail with the key (HTTP Basic, `composer config http-basic`); Unity's Package Manager sends the key alone (`token` in .upmconfig.toml: a Bearer) or both (`_auth`).

    3 methods

  • LicenseIssuer class

    Issues licences (from a paid offer, or by hand) and the keys of their seats.

    3 methods

  • LicenseKey class

    Licence keys: "glk_" and 32 characters of Crockford base32 (160 bits), grouped by four for reading aloud. The prefix makes a leaked key easy to recognise, and the Composer repository accepts it as the password.

    2 methods · 1 constant

  • LicenseSeats class

    Gives a licence's seats to e-mails, and takes them back. The one it is given to gets an e-mail: what they may now download, and how - sign in, or create their account, with that address.

    4 methods

  • LicenseToken class

    The proof an activated machine keeps, to run without asking again: what the licence allows, for that machine, until when - signed with the studio's Ed25519 key, so the software checks it offline with the public key it ships with, and nothing it holds can forge one.

    6 methods

  • PipelineGraph class

    A pipeline as a graph: a node per job, ranked by its stage, in the state of its status; an edge from each job it waits for - the ones it names (`needs`), or every job of the stage before.

    3 methods

  • ProjectReviews class

    Asking a client what they think of their project: it is closed - delivered, its board no longer moves -, and they get an e-mail with their own link to the form: one to five stars, a few words.

    2 methods

  • QuoteStatusGuard class

    Small predicates on a quote's life: omnibase/marketplace's (Base\Marketplace\Service\QuoteStatusGuard), kept under this name for the code that uses it.

    1 method

  • QuoteToOrder class

    The client accepts a quote: it becomes a one-off HourPack of the support store - its hours, its discounted total, one in stock - dropped in their cart. From there it is an order like any other; OrderPaidSubscriber credits the hours and marks the quote paid once it is.

    2 methods

  • ReleaseSync class

    Tags become releases: for each software with a repository, every tag that reads as a version (v1.2.3, 1.2, 2.0.0-beta1) and has no release yet gets one - its commit, its message as changelog, its date as publication - and its zip, and its UPM tarball when the software is a Unity package.

    3 methods · 1 constant

  • UpmRegistry class

    The studio's Unity registry: the npm registry protocol Unity's Package Manager reads from a scoped registry, over the Unity packages among the software (Software::$upmName) - ComposerIndex's sibling. Free packages for anyone; licensed ones for the holder of a valid licence, each version its licence covers.

    9 methods

  • WebglBuilds class

    A game's WebGL build, played in its page: its zip unpacked once, beside it in the storage (<software>/<version>/webgl/play/), and each file served with the headers Unity's loader needs.

    4 methods

Base\Forge\Twig

  • ForgeExtension class

    {{ 210|forge_hours }} → 3h30; {{ 2500|forge_money('EUR') }} → 25,00 €; {{ 2500|forge_price('EUR') }} → 25,00 € ≈ 27,30 $US for a visitor who reads dollars (base-bundle's Trading rendered currency, with its rate).

    4 methods

  • PipelineExtension class

    What the pipelines' templates ask: a project's latest runs, and a run as a graph (Base\Forge\Service\PipelineGraph).

    4 methods

Base\Forge

ForgeBundle class

class ForgeBundle extends AbstractBaseBundle ForgeBundle.php:15

The studio's forge on top of base-bundle-market: software and its releases (built from git tags through git/git-bundle), free and licensed downloads, a private Composer repository, client projects with a shared board and logged time, prepaid support hours and quotes.

ForgeBundle::__construct()

public function __construct()

ForgeBundle::getPath()

public function getPath(): string

Modern bundle layout: the class lives in src/, the bundle root is the package root - so TwigBundle picks up ./templates as @Forge and Doctrine's auto_mapping ./src/Entity.

ForgeBundle::build()

public function build(ContainerBuilder $container): void

Base\Forge\Command

ArtifactAttachCommand class

class ArtifactAttachCommand extends Command ArtifactAttachCommand.php:26

A build made elsewhere (Unity, a CI) becomes a release's artifact for a platform - the release made when it does not exist yet (a game need not come from a repository's tags):

bin/console forge:artifact:attach my-game 1.0.0 webgl build/webgl.zip --publish --changelog "First playable"

ArtifactAttachCommand::__construct()

public function __construct(readonly SoftwareRepository $software, readonly ArtifactBuilder $builder, readonly EntityManagerInterface $entityManager)

HoursLowBalanceCommand class

class HoursLowBalanceCommand extends Command HoursLowBalanceCommand.php:22

Tells the clients whose support hours run low (under forge.low_balance_minutes, or overdrawn) - weekly, by cron.

HoursLowBalanceCommand::__construct()

public function __construct(readonly HourCreditRepository $credits, readonly EntityManagerInterface $entityManager, readonly MailerInterface $mailer, readonly int $threshold = 120)

LicenseExpireCommand class

class LicenseExpireCommand extends Command LicenseExpireCommand.php:19

Marks the licences past their expiry as expired. They already stopped working at that instant (License::isValid() reads the date); this keeps the status honest for the back office and the account page.

LicenseExpireCommand::__construct()

public function __construct(readonly LicenseRepository $licenses, readonly EntityManagerInterface $entityManager)

LicenseKeypairCommand class

class LicenseKeypairCommand extends Command LicenseKeypairCommand.php:18

A new Ed25519 pair for the activation tokens: the secret goes in the vault (FORGE_LICENSE_SIGNING_KEY), the public key in the software. A new pair makes every token issued so far unverifiable: the software asks again, online.

LicenseKeypairCommand::__construct()

public function __construct(readonly LicenseToken $tokens)

ReleaseSyncCommand class

class ReleaseSyncCommand extends Command ReleaseSyncCommand.php:22

New tags become releases: fetch each software's repository (when it has a url), then create a release and its zip for every version tag not seen yet. Run hourly by cron; make releases runs it by hand.

ReleaseSyncCommand::__construct()

public function __construct(readonly SoftwareRepository $software, readonly ReleaseSync $sync, readonly RepositoryRegistry $registry, readonly RepositoryWarmer $warmer)

Base\Forge\Controller\Admin

ActionController class

class ActionController extends AbstractController ActionController.php:29

The back office's buttons that are not a form: rebuild a release's archive, send a quote, ask a client for their review of a delivered project, and mark an order paid when its bank transfer arrives (the manual gateway leaves it pending; confirming it here is what credits the hours and issues the licences).

ActionController::__construct()

public function __construct(readonly EntityManagerInterface $entityManager)

ActionController::build()

public function build(Request $request, int $id, ReleaseSync $sync): RedirectResponse

ActionController::send()

public function send(Request $request, int $id, MailerInterface $mailer): RedirectResponse

ActionController::review()

public function review(Request $request, int $id, ProjectReviews $reviews): RedirectResponse

Closes the project - delivered - and asks its client for a review, by e-mail.

ActionController::paid()

public function paid(Request $request, string $reference, Checkout $checkout): RedirectResponse

BuildController class

class BuildController extends AbstractController BuildController.php:26

A release's builds, one per platform (a game's, an application's): each with its size and its sha256, and a form to upload one - it replaces the platform's previous build (ArtifactBuilder::attach()). What forge:artifact:attach does from a CI.

BuildController::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly ArtifactBuilder $builder, readonly AdminContext $adminContext, readonly MenuBuilder $menuBuilder)

BuildController::builds()

public function builds(Request $request, int $id): Response

PipelineController class

class PipelineController extends AbstractController PipelineController.php:19

The pipelines in the back office: the latest runs of every project, and one of them as its graph - its jobs by stage, in the colour of their status (@glitchr/graphjs, which the back office's layout loads).

PipelineController::__construct()

public function __construct(readonly PipelineRepository $pipelines, readonly AdminContext $adminContext, readonly MenuBuilder $menuBuilder)

PipelineController::index()

public function index(): Response

PipelineController::show()

public function show(int $id): Response

RefundController class

class RefundController extends AbstractController RefundController.php:22

Refunding hours a client bought (HourRefunds): what the order paid and what is left of its hours, an amount and the hours to take back - suggested from what is unused -, and the explanation the client receives. Nothing is sent without one.

RefundController::__construct()

public function __construct(readonly HourRefunds $refunds, readonly AdminContext $adminContext, readonly MenuBuilder $menuBuilder)

RefundController::refund()

public function refund(Request $request, string $reference): Response

Base\Forge\Controller\Admin\Crud

CardCrudController class

class CardCrudController extends AbstractCrudController CardCrudController.php:16

The cards of the project boards (the clients move them from their dashboard).

CardCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

CardCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

CardCrudController::configureFields()

public function configureFields(string $pageName): iterable

DownloadCrudController class

class DownloadCrudController extends AbstractCrudController DownloadCrudController.php:19

Who downloaded what, when, through which licence and which way (the site or Composer): the log the downloads write themselves - to read, not to edit.

DownloadCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

DownloadCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

DownloadCrudController::configureActions()

public function configureActions(Actions $actions): Actions

DownloadCrudController::configureFields()

public function configureFields(string $pageName): iterable

HourCreditCrudController class

class HourCreditCrudController extends AbstractCrudController HourCreditCrudController.php:21

The support-hour ledgers, read-only: lines come from paid orders and logged time; a purchase has a "Rembourser" action. A manual correction is an adjustment line from the forge_admin_hours_adjust action (to keep the history honest).

HourCreditCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

HourCreditCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

HourCreditCrudController::configureActions()

public function configureActions(Actions $actions): Actions

HourCreditCrudController::configureFields()

public function configureFields(string $pageName): iterable

HourPackCrudController class

class HourPackCrudController extends ProductCrudController HourPackCrudController.php:11

Hour packs: a market product (store, price, stock) plus the hours it credits.

HourPackCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

HourPackCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

HourPackCrudController::configureFields()

public function configureFields(string $pageName): iterable

HourRefundCrudController class

class HourRefundCrudController extends AbstractCrudController HourRefundCrudController.php:16

The refunds made, read-only: they are made from a purchase (RefundController).

HourRefundCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

HourRefundCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

HourRefundCrudController::configureActions()

public function configureActions(Actions $actions): Actions

HourRefundCrudController::configureFields()

public function configureFields(string $pageName): iterable

LicenseActivationCrudController class

class LicenseActivationCrudController extends AbstractCrudController LicenseActivationCrudController.php:20

The machines the licensed software is activated on, seat by seat: made by the software itself (the licence API), freed here by deleting - its token runs out, and it must ask again. To give more places, raise the licence's machines per seat.

LicenseActivationCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

LicenseActivationCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

LicenseActivationCrudController::configureActions()

public function configureActions(Actions $actions): Actions

LicenseActivationCrudController::configureFields()

public function configureFields(string $pageName): iterable

LicenseCrudController class

class LicenseCrudController extends AbstractCrudController LicenseCrudController.php:21

Licences: issued when an offer is paid; granted by hand here (a gift, a partner, a client's team) - then give its seats, an e-mail each, under "Postes". Keys are never shown: each seat's holder makes theirs from their account.

LicenseCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

LicenseCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

LicenseCrudController::configureFields()

public function configureFields(string $pageName): iterable

LicenseOfferCrudController class

class LicenseOfferCrudController extends ProductCrudController LicenseOfferCrudController.php:11

Licence offers: a market product that issues a licence of its software per unit paid.

LicenseOfferCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

LicenseOfferCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

LicenseOfferCrudController::configureFields()

public function configureFields(string $pageName): iterable

LicenseSeatCrudController class

class LicenseSeatCrudController extends AbstractCrudController LicenseSeatCrudController.php:20

The seats of the licences: who may use each - an e-mail. Saving one tells its holder by e-mail (LicenseSeatListener); whoever signs in with that address, verified, downloads and makes their key. Deleting it takes the access back at once. Not more seats than the licence has: raise its number first.

LicenseSeatCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

LicenseSeatCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

LicenseSeatCrudController::configureFields()

public function configureFields(string $pageName): iterable

PipelineCrudController class

class PipelineCrudController extends AbstractCrudController PipelineCrudController.php:17

The pipelines recorded: which project, which commit, where they ran - and each as its graph.

PipelineCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

PipelineCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

PipelineCrudController::configureActions()

public function configureActions(Actions $actions): Actions

PipelineCrudController::configureFields()

public function configureFields(string $pageName): iterable

ProjectCrudController class

class ProjectCrudController extends AbstractCrudController ProjectCrudController.php:18

Client projects: who, which repository, how many hours agreed.

ProjectCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

ProjectCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

ProjectCrudController::configureActions()

public function configureActions(Actions $actions): Actions

ProjectCrudController::configureFields()

public function configureFields(string $pageName): iterable

ProjectReviewCrudController class

class ProjectReviewCrudController extends AbstractCrudController ProjectReviewCrudController.php:23

The clients' reviews of their delivered projects: asked from the projects ("Demander un avis"), given by the client. Tick "Affiché" to show one on the site; the signature can be shortened, the words are the client's.

ProjectReviewCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

ProjectReviewCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

ProjectReviewCrudController::configureActions()

public function configureActions(Actions $actions): Actions

ProjectReviewCrudController::configureFields()

public function configureFields(string $pageName): iterable

QuoteCrudController class

class QuoteCrudController extends AbstractCrudController QuoteCrudController.php:28

Quotes and special offers: requests arrive from the site; price the lines, set a discount and a date, then "Send" mails the client their link.

QuoteCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

QuoteCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

QuoteCrudController::configureActions()

public function configureActions(Actions $actions): Actions

QuoteCrudController::configureFields()

public function configureFields(string $pageName): iterable

QuoteCrudController::createEntity()

public function createEntity(string $entityFqcn): object

QuoteCrudController::persistEntity()

public function persistEntity(EntityManagerInterface $entityManager, object $entity): void

Numbered when saved, not when the form opens: two forms open took the same number.

ReleaseCrudController class

class ReleaseCrudController extends AbstractCrudController ReleaseCrudController.php:20

Releases: made from tags by forge:release:sync, or by hand; "Build" (re)makes the zip (and a Unity package's tarball); "Builds" lists and uploads a game's or an application's builds, one per platform.

ReleaseCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

ReleaseCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

ReleaseCrudController::configureActions()

public function configureActions(Actions $actions): Actions

ReleaseCrudController::configureFields()

public function configureFields(string $pageName): iterable

SoftwareCrudController class

class SoftwareCrudController extends AbstractCrudController SoftwareCrudController.php:27

The software (base-bundle threads: translated title, tagline and description; published, they are on the applications page).

SoftwareCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

SoftwareCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

SoftwareCrudController::configureFields()

public function configureFields(string $pageName): iterable

TimeEntryCrudController class

class TimeEntryCrudController extends AbstractCrudController TimeEntryCrudController.php:18

Time spent on client projects. Every billable entry comes off the client's support hours in the same save (TimeEntryLedgerSubscriber).

TimeEntryCrudController::getEntityFqcn()

public static function getEntityFqcn(): string

TimeEntryCrudController::getPreferredIcon()

public static function getPreferredIcon(): ?string

TimeEntryCrudController::configureFields()

public function configureFields(string $pageName): iterable

TimeEntryCrudController::createEntity()

public function createEntity(string $entityFqcn): object

Base\Forge\Controller\Client

AccountController class

class AccountController extends AbstractController AccountController.php:28

The client's side of the forge: their licences, the seats they give and the keys of the ones they hold, how to install from the Composer repository, their support hours, their quotes and what they downloaded.

AccountController::__construct()

public function __construct(readonly LicenseRepository $licenses, readonly QuoteRepository $quotes, readonly HourLedger $ledger, readonly EntityManagerInterface $entityManager)

AccountController::licenses()

public function licenses(): Response

AccountController::rekey()

public function rekey(Request $request, int $id, LicenseIssuer $issuer): Response

A new key for the signed-in user's seat, shown once on the page that answers; the old one stops working.

AccountController::give()

public function give(Request $request, int $id, LicenseSeats $seats): Response

The owner gives a seat to an e-mail - who is told by e-mail.

AccountController::takeBack()

public function takeBack(Request $request, int $id, int $seat, LicenseSeats $seats): Response

The owner takes a seat back: its key stops working at once.

AccountController::releaseMachine()

public function releaseMachine(Request $request, int $id, int $activation, LicenseActivations $activations): Response

Frees a machine's place: the seat's holder, for their own, and the licence's owner, for their team's. The software asks again at its next start, and finds the place free for another machine.

BoardController class

class BoardController extends AbstractController BoardController.php:27

A project's board: the page that shows it, and the move of a card - what its script (public/js/board.js) sends at each drop. Who sees a project and who moves its cards is ProjectVoter's: its client and the staff; a delivered project no longer moves for its client.

An application with a dashboard of its own includes the board there (@Forge/client/project/_board.html.twig) and needs nothing else: the move comes here.

const TOKEN = 'forge_board'

The id of the CSRF token a board carries (data-token) and its moves send back (X-CSRF-Token).

BoardController::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly Board $board)

BoardController::show()

public function show(int $id): Response

BoardController::move()

public function move(int $id, Request $request, CardMove $move): JsonResponse

ComposerController class

class ComposerController extends AbstractController ComposerController.php:19

The Composer repository's single document. Anonymous: the free packages.

With HTTP Basic credentials (the e-mail of a licence seat + its key, what composer config http-basic.<host> stores): the licensed packages the licences with a seat for that e-mail cover too. Wrong credentials are a 401, so Composer asks again rather than silently resolving without them.

ComposerController::__construct()

public function __construct(readonly ComposerIndex $index)

ComposerController::packages()

public function packages(Request $request): Response

DownloadController class

class DownloadController extends AbstractController DownloadController.php:29

The software page and its downloads. A download is two steps: the version link checks who you are against the licence (ArtifactVoter) and redirects to a signed, short-lived file URL; that URL is served by nginx (X-Accel-Redirect to forge.accel_prefix) or, without it, by PHP.

DownloadController::__construct()

public function __construct(readonly SoftwareRepository $software, readonly LicenseRepository $licenses, readonly EntityManagerInterface $entityManager, readonly DownloadLinks $links, readonly ArtifactFiles $files, readonly int $downloadTtl = 600)

DownloadController::index()

public function index(): Response

DownloadController::software()

public function software(string $slug): Response

DownloadController::download()

public function download(Request $request, string $slug, string $version): Response

?format= another of the release's artifacts than its zip: a platform's build (webgl, windows, macos, linux, android), the UPM tarball (upm).

DownloadController::serve()

public function serve(Request $request, int $id): Response

HubController class

class HubController extends AbstractController HubController.php:22

What the hub clients read beside the Unity registry (docs/hub.md): the hub manifest, the catalogue - its paid packages too, with their offers, and the games -, and the device flow that links an editor to its user's licences once bought (DeviceLinks). Under /upm, whose firewall the host leaves open: the licence key comes as the registry's Bearer token, the device code in the body.

HubController::__construct()

public function __construct(readonly HubCatalog $catalog, readonly UpmRegistry $registry, readonly DeviceLinks $links, readonly bool $checkout = true)

HubController::manifest()

public function manifest(): JsonResponse

HubController::catalog()

public function catalog(Request $request): JsonResponse

HubController::licenses()

public function licenses(Request $request): JsonResponse

The key's licences and the machines of its seat (a hub client's Licences page).

HubController::device()

public function device(Request $request): JsonResponse

The editor asks for a code to show its user; the link page takes it.

HubController::token()

public function token(Request $request): JsonResponse

The editor asks, every interval, whether its user linked it: the key once, else why not.

HubLinkController class

class HubLinkController extends AbstractController HubLinkController.php:21

The page a hub client sends its user to (the device flow, DeviceLinks): signed in, they see the code their editor shows, the item it asked for with its offers - bought here, in a tab of their own, through the shop's cart and checkout - and, once they hold a licence, "Link this editor": the editor then receives a key of theirs and installs.

HubLinkController::__construct()

public function __construct(readonly DeviceLinks $links, readonly SoftwareRepository $software)
public function link(Request $request): Response

LicenseApiController class

class LicenseApiController extends AbstractController LicenseApiController.php:21

The licence API the studio's software calls (a Unity game, a desktop application): activate a machine and get a signed token to run offline; free the machine; the public key to check the tokens with. JSON, no session: the seat's key is the credential. See docs/licensing.md.

LicenseApiController::__construct()

public function __construct(readonly LicenseActivations $activations)

LicenseApiController::activate()

public function activate(ActivationRequest $request): JsonResponse

Activates the machine, or refreshes it: 200 and a token; 401 a wrong key; 403 another software's key, or a licence expired or revoked; 409 every place taken - with the machines that hold them.

LicenseApiController::release()

public function release(ReleaseRequest $request): JsonResponse

Frees the machine's place: 200 whether it held one or not; 401 a wrong key.

LicenseApiController::publicKey()

public function publicKey(LicenseToken $tokens): JsonResponse

The key to check the tokens with - to ship in the software, or to fetch once.

PlayController class

class PlayController extends AbstractController PlayController.php:25

A game's WebGL build, played in its page (the software page frames /play/<slug>/<version>/ once the visitor presses Play): the build's own index.html, then its loader and its compressed files, with the headers WebglBuilds gives them. A free game is played in the browser by anyone; a paid one is downloaded once bought, and played here only when the studio opened a playable preview of it (Software::$playablePreview) - then by anyone too. The staff always, to check a build.

PlayController::__construct()

public function __construct(readonly SoftwareRepository $software, readonly WebglBuilds $builds)

PlayController::play()

public function play(Request $request, string $slug, string $version, string $path): Response

ReviewController class

class ReviewController extends AbstractController ReviewController.php:20

Where the client's review link lands: their project, delivered and closed, and the form - one to five stars, a few words. Their link is the credential: no account needed. Answered once; then it thanks them.

ReviewController::review()

public function review(string $token, Request $request, ProjectReviewRepository $reviews, EntityManagerInterface $entityManager): Response

ShopController class

class ShopController extends AbstractController ShopController.php:33

Support, sold by the hour: the packs of the support store, the special offers (quotes) made to the signed-in client, and the form asking for one.

A quote is read and accepted through its own link (/devis/<token>).

ShopController::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly QuoteRepository $quotes, readonly HourLedger $ledger, readonly string $supportStore = 'support')

ShopController::index()

public function index(): Response

ShopController::request()

public function request(Request $request, MailerInterface $mailer, CompanyRegistry $registry, ?string $contact = null): Response

ShopController::quote()

public function quote(string $token): Response

ShopController::accept()

public function accept(Request $request, string $token, QuoteToOrder $quoteToOrder): Response

UpmController class

class UpmController extends AbstractController UpmController.php:24

The Unity registry (UpmRegistry), as Unity's Package Manager reads a scoped registry: the search, the package documents, the tarballs.

Anonymous: the free packages. With the licence key in an Authorization header (token in .upmconfig.toml: a Bearer; _auth: Basic e-mail:key), the licensed packages the licences with a seat for that key's e-mail cover too. A wrong key is a 401, so that the Package Manager says so; a licensed package asked without a key is a 401 too, with a key that does not cover it a 403.

UpmController::__construct()

public function __construct(readonly UpmRegistry $registry, readonly ArtifactFiles $files)
public function search(Request $request): Response

UpmController::all()

public function all(Request $request): Response

UpmController::package()

public function package(Request $request, string $name): Response

UpmController::tarball()

public function tarball(Request $request, string $name, string $filename): Response

Base\Forge\DependencyInjection

ForgeConfiguration class

class ForgeConfiguration implements ConfigurationInterface ForgeConfiguration.php:8

ForgeConfiguration::getConfigTreeBuilder()

public function getConfigTreeBuilder(): TreeBuilder

ForgeExtension class

class ForgeExtension extends AbstractBaseExtension ForgeExtension.php:11

ForgeExtension::getConfiguration()

public function getConfiguration(array $config, ContainerBuilder $container): ForgeConfiguration

ForgeExtension::load()

public function load(array $configs, ContainerBuilder $container): void

Base\Forge\Entity

Artifact class

class Artifact implements Stringable Artifact.php:16

A file a release is downloaded as, stored under forge.storage_dir (never in the web root) and handed out by DownloadController. Its format says what it is: "zip", the tree at the tag (ArtifactBuilder); "upm", the same tree as the npm tarball Unity's Package Manager installs (UpmRegistry); or a platform's value (Platform: webgl, windows, macos, linux, android), the build of a game or an application for it.

Artifact::__construct()

public function __construct(?Release $release = null, string $path = '', string $format = 'zip', int $size = 0, string $sha256 = '', ?string $sha1 = null, ?string $sha512 = null)

Artifact::__toString()

public function __toString(): string

Artifact::getId()

public function getId(): ?int

Artifact::getRelease()

public function getRelease(): ?Release

Artifact::setRelease()

public function setRelease(?Release $release): self

Artifact::getSoftware()

public function getSoftware(): ?Software

Artifact::getPath()

public function getPath(): string

Artifact::getFilename()

public function getFilename(): string

Artifact::getFormat()

public function getFormat(): string

Artifact::getSize()

public function getSize(): int

Artifact::getSha256()

public function getSha256(): string

Artifact::getSha1()

public function getSha1(): ?string

Artifact::getSha512()

public function getSha512(): ?string

Artifact::getIntegrity()

public function getIntegrity(): ?string

Subresource integrity, as npm writes it: sha512-<base64>.

Artifact::getPlatform()

public function getPlatform(): ?Platform

The platform it is the build for, or null: a source archive, a package.

Artifact::getContentType()

public function getContentType(): string

What it is served as.

Artifact::getCreatedAt()

public function getCreatedAt(): \DateTimeImmutable

Artifact::isFree()

public function isFree(): bool

Card class

class Card implements Stringable Card.php:10

A task on a project's board. The client and the studio both move them.

Card::__construct()

public function __construct(string $title = '', CardColumn $column = CardColumn::BACKLOG)

Card::__toString()

public function __toString(): string

Card::getId()

public function getId(): ?int

Card::getProject()

public function getProject(): ?Project

Card::setProject()

public function setProject(?Project $project): self

Card::getTitle()

public function getTitle(): string

Card::setTitle()

public function setTitle(string $title): self

Card::getDescription()

public function getDescription(): ?string

Card::setDescription()

public function setDescription(?string $description): self

Card::getColumn()

public function getColumn(): CardColumn

Card::getPosition()

public function getPosition(): int

Card::moveTo()

public function moveTo(CardColumn $column, int $position): self

Card::setColumn()

public function setColumn(CardColumn $column): self

Card::setPosition()

public function setPosition(int $position): self

Card::getLabel()

public function getLabel(): ?string

Card::setLabel()

public function setLabel(?string $label): self

Card::getUpdatedAt()

public function getUpdatedAt(): \DateTimeImmutable

Download class

class Download Download.php:9

One artifact handed out: who, under which licence, when. The IP is kept hashed.

Download::__construct()

public function __construct(Artifact $artifact, ?User $user, ?License $license, ?string $ipHash, string $channel = 'web')

Download::getId()

public function getId(): ?int

Download::getArtifact()

public function getArtifact(): Artifact

Download::getUser()

public function getUser(): ?User

Download::getLicense()

public function getLicense(): ?License

Download::getChannel()

public function getChannel(): string

Download::getDownloadedAt()

public function getDownloadedAt(): \DateTimeImmutable

HourCredit class

class HourCredit HourCredit.php:15

One line of a client's support-hour ledger: positive when hours are bought, negative when time is logged. The balance is their sum; nothing else stores it, so it can never drift from its history.

HourCredit::__construct()

public function __construct(User $user, int $minutes, CreditReason $reason, ?string $note = null)

HourCredit::getId()

public function getId(): ?int

HourCredit::getUser()

public function getUser(): User

HourCredit::setUser()

public function setUser(User $user): self

A time entry's debit follows it to another client's project (TimeEntryLedgerSubscriber).

HourCredit::getMinutes()

public function getMinutes(): int

HourCredit::setMinutes()

public function setMinutes(int $minutes): self

HourCredit::getReason()

public function getReason(): CreditReason

HourCredit::getNote()

public function getNote(): ?string

HourCredit::getOrderReference()

public function getOrderReference(): ?string

HourCredit::setOrderReference()

public function setOrderReference(?string $orderReference): self

HourCredit::getTimeEntry()

public function getTimeEntry(): ?TimeEntry

HourCredit::setTimeEntry()

public function setTimeEntry(?TimeEntry $timeEntry): self

HourCredit::getCreatedAt()

public function getCreatedAt(): \DateTimeImmutable

HourRefund class

class HourRefund implements Stringable HourRefund.php:16

Hours bought, paid back: the studio keeps the right to refund a purchase - when the work asked for does not sit with the client's values, or the studio's - and says why. The amount goes back on the card through Stripe (or by bank transfer, by hand, for the manual gateway); the hours it covered leave the ledger (a refund line); the client is told, with the explanation.

const STRIPE = 'stripe'
const TRANSFER = 'transfer'

HourRefund::__construct()

public function __construct(?User $customer, string $orderReference, int $amount, string $currency, int $minutes, string $reason, string $method)

HourRefund::__toString()

public function __toString(): string

HourRefund::getId()

public function getId(): ?int

HourRefund::getCustomer()

public function getCustomer(): ?User

HourRefund::getOrderReference()

public function getOrderReference(): string

HourRefund::getAmount()

public function getAmount(): int

HourRefund::getCurrency()

public function getCurrency(): string

HourRefund::getMinutes()

public function getMinutes(): int

HourRefund::getReason()

public function getReason(): string

HourRefund::getMethod()

public function getMethod(): string

HourRefund::isByTransfer()

public function isByTransfer(): bool

HourRefund::getStripeRefund()

public function getStripeRefund(): ?string

HourRefund::setStripeRefund()

public function setStripeRefund(?string $stripeRefund): self

HourRefund::getRefundedBy()

public function getRefundedBy(): ?User

HourRefund::setRefundedBy()

public function setRefundedBy(?User $refundedBy): self

HourRefund::getCreatedAt()

public function getCreatedAt(): \DateTimeImmutable

Job class

class Job implements Stringable Job.php:9

One thing a stage runs: a test suite, a build, a deployment.

Job::__construct()

public function __construct(string $name = '', PipelineStatus $status = PipelineStatus::PENDING)

Job::__toString()

public function __toString(): string

Job::getId()

public function getId(): ?int

Job::getStage()

public function getStage(): ?Stage

Job::setStage()

public function setStage(?Stage $stage): self

Job::getName()

public function getName(): string

Job::setName()

public function setName(string $name): self

Job::getStatus()

public function getStatus(): PipelineStatus

Job::setStatus()

public function setStatus(PipelineStatus $status): self

Job::getEffectiveStatus()

public function getEffectiveStatus(): PipelineStatus

Its status as its stage counts it: a failure that was allowed is a warning.

Job::isAllowFailure()

public function isAllowFailure(): bool

Job::setAllowFailure()

public function setAllowFailure(bool $allowFailure): self

Job::getNeeds()

public function getNeeds(): array

Job::setNeeds()

public function setNeeds(array $needs): self

Job::getUrl()

public function getUrl(): ?string

Job::setUrl()

public function setUrl(?string $url): self

Job::getStartedAt()

public function getStartedAt(): ?\DateTimeImmutable

Job::setStartedAt()

public function setStartedAt(?\DateTimeImmutable $startedAt): self

Job::getFinishedAt()

public function getFinishedAt(): ?\DateTimeImmutable

Job::setFinishedAt()

public function setFinishedAt(?\DateTimeImmutable $finishedAt): self

Job::getDuration()

public function getDuration(): ?int

Seconds it ran; null before it ends.

License class

class License implements Stringable License.php:21

The right to download a licensed piece of software - and to install it from the Composer repository. Granted when a LicenseOffer is paid for, or by hand from the back office.

It has seats: that many e-mails may use it, one LicenseSeat each, with a key of their own. Its owner - who bought it, or whom the studio gave it to - gives the seats and takes them back; owning it is not holding one.

License::__construct()

public function __construct(?Software $software = null, ?User $owner = null)

License::__toString()

public function __toString(): string

License::getId()

public function getId(): ?int

License::getSoftware()

public function getSoftware(): ?Software

License::setSoftware()

public function setSoftware(Software $software): self

License::getOwner()

public function getOwner(): ?User

License::setOwner()

public function setOwner(User $owner): self

License::getHolders()

public function getHolders(): Collection
Returns

the e-mails it is given to, first given first

License::getMachinesPerSeat()

public function getMachinesPerSeat(): int

License::setMachinesPerSeat()

public function setMachinesPerSeat(int $machines): self

License::getSeatsTaken()

public function getSeatsTaken(): int

License::getSeatsLeft()

public function getSeatsLeft(): int

License::findSeat()

public function findSeat(string $email): ?LicenseSeat

The seat given to that e-mail, if any.

License::findSeatOf()

public function findSeatOf(User $user): ?LicenseSeat

The seat that user holds, if any.

License::addHolder()

public function addHolder(LicenseSeat $seat): self

Gives a seat to that e-mail - not checked against the seats left: LicenseSeats does.

License::removeHolder()

public function removeHolder(LicenseSeat $seat): self

License::getStatus()

public function getStatus(): LicenseStatus

License::setStatus()

public function setStatus(LicenseStatus $status): self

License::getSeats()

public function getSeats(): int

License::setSeats()

public function setSeats(int $seats): self

Never fewer than the seats already given: take some back first.

License::getExpiresAt()

public function getExpiresAt(): ?\DateTimeImmutable

License::setExpiresAt()

public function setExpiresAt(?\DateTimeImmutable $expiresAt): self

License::getUpdatesUntil()

public function getUpdatesUntil(): ?\DateTimeImmutable

License::setUpdatesUntil()

public function setUpdatesUntil(?\DateTimeImmutable $updatesUntil): self

License::getOrderReference()

public function getOrderReference(): ?string

License::setOrderReference()

public function setOrderReference(?string $orderReference): self

License::getCreatedAt()

public function getCreatedAt(): \DateTimeImmutable

License::isValid()

public function isValid(?\DateTimeImmutable $at = null): bool

License::covers()

public function covers(Release $release, ?\DateTimeImmutable $at = null): bool

Whether this licence opens that release: valid, and the release within its updates.

LicenseActivation class

class LicenseActivation implements Stringable LicenseActivation.php:19

One machine a seat's software is activated on: a game, an application installed on a computer. The licence allows so many machines a seat (License::$machinesPerSeat); whoever holds the seat frees one from their account - a new computer, an old one gone - and the studio from the back office.

The machine is known by a hash of the identifier its software sends (Unity's deviceUniqueIdentifier, macOS's IOPlatformUUID...): the raw hardware identifier is never stored. Its name and platform are what the software reports, to recognise it by in a list.

LicenseActivation::__construct()

public function __construct(?LicenseSeat $seat = null, string $machine = '')

LicenseActivation::hash()

public static function hash(string $identifier): string

The hash a machine is known by: the one its software can compute too.

LicenseActivation::__toString()

public function __toString(): string

LicenseActivation::getId()

public function getId(): ?int

LicenseActivation::getSeat()

public function getSeat(): ?LicenseSeat

LicenseActivation::setSeat()

public function setSeat(?LicenseSeat $seat): self

LicenseActivation::getMachine()

public function getMachine(): string

LicenseActivation::getName()

public function getName(): ?string

LicenseActivation::setName()

public function setName(?string $name): self

LicenseActivation::getPlatform()

public function getPlatform(): ?string

LicenseActivation::setPlatform()

public function setPlatform(?string $platform): self

LicenseActivation::getVersion()

public function getVersion(): ?string

LicenseActivation::setVersion()

public function setVersion(?string $version): self

LicenseActivation::getActivatedAt()

public function getActivatedAt(): \DateTimeImmutable

LicenseActivation::getLastSeenAt()

public function getLastSeenAt(): \DateTimeImmutable

LicenseActivation::seen()

public function seen(?\DateTimeImmutable $at = null): self

LicenseSeat class

class LicenseSeat implements Stringable LicenseSeat.php:23

One seat of a licence: the e-mail it is given to. Whoever signs in with that e-mail - verified - downloads what the licence covers and installs it with Composer, with a key of their own. The licence's owner gives its seats, and takes them back; a paid licence starts with the owner's.

The account is linked on the first match (a seat given to someone who has none yet waits for them); the key is shown once, only its SHA-256 is stored, plus a short prefix to recognise it by.

LicenseSeat::__construct()

public function __construct(?License $license = null, string $email = '', ?User $user = null)

LicenseSeat::__toString()

public function __toString(): string

LicenseSeat::getId()

public function getId(): ?int

LicenseSeat::getLicense()

public function getLicense(): ?License

LicenseSeat::setLicense()

public function setLicense(?License $license): self

LicenseSeat::getEmail()

public function getEmail(): string

LicenseSeat::setEmail()

public function setEmail(string $email): self

LicenseSeat::getUser()

public function getUser(): ?User

LicenseSeat::setUser()

public function setUser(?User $user): self

LicenseSeat::getAssignedAt()

public function getAssignedAt(): \DateTimeImmutable

LicenseSeat::getActivations()

public function getActivations(): Collection

LicenseSeat::findActivation()

public function findActivation(string $machine): ?LicenseActivation

LicenseSeat::addActivation()

public function addActivation(LicenseActivation $activation): self

LicenseSeat::removeActivation()

public function removeActivation(LicenseActivation $activation): self

LicenseSeat::getMachinesLeft()

public function getMachinesLeft(): int

Machines it may still be activated on.

LicenseSeat::isNotified()

public function isNotified(): bool

LicenseSeat::markNotified()

public function markNotified(): self

LicenseSeat::isHeldBy()

public function isHeldBy(User $user): bool

Whether this seat is that user's: linked to them, or given to their e-mail - once they have proved it is theirs.

LicenseSeat::getKeyPrefix()

public function getKeyPrefix(): ?string

LicenseSeat::hasKey()

public function hasKey(): bool

LicenseSeat::setKey()

public function setKey(string $key): self

Stores the key's hash; the key itself is the caller's to show once.

LicenseSeat::validateSeats()

public function validateSeats(ExecutionContextInterface $context): void

Not more seats given than the licence has.

LicenseSeat::matchesKey()

public function matchesKey(string $key): bool

Pipeline class

class Pipeline implements Stringable Pipeline.php:16

One run of a project's checks on a commit - its tests, its build, its deployment -, wherever it ran (a forge's CI, a script): its stages in order, each with its jobs. The forge keeps what happened and shows it (a graph, drawn by @glitchr/graphjs); it runs nothing itself.

Pipeline::__construct()

public function __construct(?Project $project = null, ?string $ref = null, ?string $commitSha = null)

Pipeline::__toString()

public function __toString(): string

Pipeline::getId()

public function getId(): ?int

Pipeline::getProject()

public function getProject(): ?Project

Pipeline::setProject()

public function setProject(?Project $project): self

Pipeline::getRef()

public function getRef(): ?string

Pipeline::setRef()

public function setRef(?string $ref): self

Pipeline::getCommitSha()

public function getCommitSha(): ?string

Pipeline::setCommitSha()

public function setCommitSha(?string $commitSha): self

Pipeline::getShortSha()

public function getShortSha(): ?string

Pipeline::getSource()

public function getSource(): ?string

Pipeline::getExternalId()

public function getExternalId(): ?string

Pipeline::setSource()

public function setSource(?string $source, ?string $externalId = null): self

Pipeline::getUrl()

public function getUrl(): ?string

Pipeline::setUrl()

public function setUrl(?string $url): self

Pipeline::getStatus()

public function getStatus(): PipelineStatus

What was set by hand, or what the stages say.

Pipeline::setStatus()

public function setStatus(?PipelineStatus $status): self

Pipeline::getCreatedAt()

public function getCreatedAt(): \DateTimeImmutable

Pipeline::setCreatedAt()

public function setCreatedAt(\DateTimeImmutable $createdAt): self

Pipeline::getFinishedAt()

public function getFinishedAt(): ?\DateTimeImmutable

Pipeline::setFinishedAt()

public function setFinishedAt(?\DateTimeImmutable $finishedAt): self

Pipeline::getDuration()

public function getDuration(): ?int

Seconds from its creation to its end; null while it runs.

Pipeline::getStages()

public function getStages(): Collection
Returns

in order

Pipeline::stage()

public function stage(string $name): Stage

The stage of that name, made at the end when there is none.

Pipeline::getJobs()

public function getJobs(): array
Returns

every job, stage after stage

Project class

class Project extends Thread Project.php:21

A client's project: what the dashboard shows them - the board, the time the studio logged on it, and the history of its git repository. A base-bundle thread: its title and excerpt are translated, its client is also its owner (base-bundle's own notion of whose a thread is).

Project::__iconizeStatic()

public static function __iconizeStatic(): ?array

Project::__construct()

public function __construct(?User $client = null, ?string $title = null, ?string $slug = null)

Project::getClient()

public function getClient(): ?User

Project::setClient()

public function setClient(User $client): self

Project::getName()

public function getName(): string

The title, as the dashboard calls it.

Project::setName()

public function setName(string $name): self

Project::getSummary()

public function getSummary(): ?string

One line under the name: the thread's (translated) excerpt.

Project::setSummary()

public function setSummary(?string $summary): self

Project::getRepositoryUrl()

public function getRepositoryUrl(): ?string

Project::setRepositoryUrl()

public function setRepositoryUrl(?string $repositoryUrl): self

Project::getGitRepository()

public function getGitRepository(): ?string

Project::setGitRepository()

public function setGitRepository(?string $gitRepository): self

Project::getStatus()

public function getStatus(): ProjectStatus

Project::setStatus()

public function setStatus(ProjectStatus $status): self

Project::getBudgetMinutes()

public function getBudgetMinutes(): ?int

Project::setBudgetMinutes()

public function setBudgetMinutes(?int $budgetMinutes): self

Project::getDueOn()

public function getDueOn(): ?\DateTimeImmutable

Project::setDueOn()

public function setDueOn(?\DateTimeImmutable $dueOn): self

Project::getCards()

public function getCards(): Collection

Project::getBoard()

public function getBoard(): array
Returns

the board, column by column, in position order

Project::addCard()

public function addCard(Card $card): self

Project::getTimeEntries()

public function getTimeEntries(): Collection
Returns

newest first

Project::addTimeEntry()

public function addTimeEntry(TimeEntry $entry): self

Project::getSpentMinutes()

public function getSpentMinutes(): int

Project::getProgress()

public function getProgress(): int

0-100: the share of cards done, or of the budget spent when there are no cards.

ProjectReview class

class ProjectReview implements Stringable ProjectReview.php:15

What the client thinks of a delivered project. The studio asks for it from the back office (the project is then closed: delivered, its board no longer moves); the client answers through their link - one to five stars, a few words -; the studio decides whether it shows on the site.

ProjectReview::__construct()

public function __construct(?Project $project = null)

ProjectReview::__toString()

public function __toString(): string

ProjectReview::getId()

public function getId(): ?int

ProjectReview::getProject()

public function getProject(): ?Project

ProjectReview::getToken()

public function getToken(): string

ProjectReview::getRequestedAt()

public function getRequestedAt(): \DateTimeImmutable

ProjectReview::renew()

public function renew(): self

Asked again (the first e-mail lost): a new link, the old one void.

ProjectReview::getRating()

public function getRating(): ?int

ProjectReview::getComment()

public function getComment(): ?string

ProjectReview::setComment()

public function setComment(?string $comment): self

ProjectReview::getSignature()

public function getSignature(): ?string

ProjectReview::setSignature()

public function setSignature(?string $signature): self

ProjectReview::getSubmittedAt()

public function getSubmittedAt(): ?\DateTimeImmutable

ProjectReview::isSubmitted()

public function isSubmitted(): bool

ProjectReview::submit()

public function submit(int $rating, ?string $comment, ?string $signature): self

The client's answer: once.

ProjectReview::isPublished()

public function isPublished(): bool

ProjectReview::setPublished()

public function setPublished(bool $published): self

Quote class

class Quote extends AbstractQuote Quote.php:23

A special offer made to one client: lines of hours at a rate, a discount, a date it holds until. Asked for from the quote form, priced in the back office, read and accepted by the client - which turns it into a one-off HourPack in their cart (Forge\Service\QuoteToOrder); paying that order credits the hours and marks the quote paid.

What every quote is (its client and company, its status, discount, validity, token) is omnibase/marketplace's AbstractQuote, mapped here on the forge's own table: the hours and the HourPack are the forge's.

Quote::__construct()

public function __construct(string $reference = '')

Quote::hasSomethingToSell()

public function hasSomethingToSell(): bool

Hours to sell: a quote without any cannot be accepted.

Quote::getLines()

public function getLines(): Collection

Quote::addLine()

public function addLine(QuoteLine $line): self

Quote::removeLine()

public function removeLine(QuoteLine $line): self

Quote::getTotalMinutes()

public function getTotalMinutes(): int

Quote::getSubtotal()

public function getSubtotal(): int

Before the discount, in cents.

Quote::getProduct()

public function getProduct(): ?HourPack

Quote::setProduct()

public function setProduct(?HourPack $product): self

Quote::getProject()

public function getProject(): ?Project

Quote::setProject()

public function setProject(?Project $project): self

QuoteLine class

class QuoteLine implements Stringable QuoteLine.php:9

One line of a quote: a piece of work, its hours, the rate.

QuoteLine::__construct()

public function __construct(string $label = '', int $minutes = 0, int $hourlyRate = 0)

QuoteLine::__toString()

public function __toString(): string

QuoteLine::getId()

public function getId(): ?int

QuoteLine::getQuote()

public function getQuote(): ?Quote

QuoteLine::setQuote()

public function setQuote(?Quote $quote): self

QuoteLine::getLabel()

public function getLabel(): string

QuoteLine::setLabel()

public function setLabel(string $label): self

QuoteLine::getMinutes()

public function getMinutes(): int

QuoteLine::setMinutes()

public function setMinutes(int $minutes): self

QuoteLine::getHours()

public function getHours(): float

Hours as the admin form edits them (1.5 = 1h30).

QuoteLine::setHours()

public function setHours(float $hours): self

QuoteLine::getHourlyRate()

public function getHourlyRate(): int

QuoteLine::setHourlyRate()

public function setHourlyRate(int $hourlyRate): self

QuoteLine::getPosition()

public function getPosition(): int

QuoteLine::setPosition()

public function setPosition(int $position): self

QuoteLine::getAmount()

public function getAmount(): int

In cents.

Release class

class Release implements Stringable Release.php:15

One version of a piece of software: a git tag, the commit it points at, the tag's message as changelog, and the archives built from that tree.

Created by forge:release:sync from the repository's tags, or by hand.

Release::__construct()

public function __construct(?Software $software = null, string $version = '')

Release::__toString()

public function __toString(): string

Release::getId()

public function getId(): ?int

Release::getSoftware()

public function getSoftware(): ?Software

Release::setSoftware()

public function setSoftware(?Software $software): self

Release::getVersion()

public function getVersion(): string

Release::setVersion()

public function setVersion(string $version): self

Release::getTag()

public function getTag(): ?string

Release::setTag()

public function setTag(?string $tag): self

Release::getCommitSha()

public function getCommitSha(): ?string

Release::setCommitSha()

public function setCommitSha(?string $commitSha): self

Release::getChangelog()

public function getChangelog(): ?string

Release::setChangelog()

public function setChangelog(?string $changelog): self

Release::getPublishedAt()

public function getPublishedAt(): ?\DateTimeImmutable

Release::setPublishedAt()

public function setPublishedAt(?\DateTimeImmutable $publishedAt): self

Release::isPublished()

public function isPublished(): bool

Release::getArtifacts()

public function getArtifacts(): Collection

Release::addArtifact()

public function addArtifact(Artifact $artifact): self

Release::getPlatformArtifacts()

public function getPlatformArtifacts(): array
Returns

its builds, one per platform, in Platform's order

Release::getArtifact()

public function getArtifact(string $format = 'zip'): ?Artifact

Software class

class Software extends Thread implements LinkableInterface Software.php:33

Something the studio made and shows: a client site, a bundle, a JavaScript library, a tool. A base-bundle thread, as market's stores and products are: its title, headline and content are translated (FR/EN), its slug and state come with it - published, it is on the applications page. With a git repository it gets releases; FREE ones download for anyone, LICENSED ones against a licence bought through one of its LicenseOffer products. Its kind says how it is delivered: a package (Composer, Unity's Package Manager), a game (played in the page, downloaded per platform) or an application.

const CATEGORIES = ['site', 'bundle', 'javascript', 'tool', 'game']

Software::__iconizeStatic()

public static function __iconizeStatic(): ?array
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?string

Software::__construct()

public function __construct(?User $owner = null, ?string $title = null, ?string $slug = null)

Software::setParent()

public function setParent(?Thread $parent): self

Its family (Thread's parent): base-bundle-admin, -market, base-plugin... under base-bundle. The pages list a family under its head, not side by side with it. Never its own ancestor.

Software::getVisibleChildren()

public function getVisibleChildren(): array
Returns

its published children, for the public pages

Software::isHead()

public function isHead(): bool

Listed on its own: no parent, or one that is not published.

Software::getName()

public function getName(): string

The title, as the templates and the admin call it.

Software::setName()

public function setName(string $name): self

Software::getTagline()

public function getTagline(): ?string

The one-liner under the name: the thread's (translated) headline.

Software::setTagline()

public function setTagline(?string $tagline): self

Software::getDescription()

public function getDescription(): ?string

The long text: the thread's (translated) content.

Software::setDescription()

public function setDescription(?string $description): self

Software::publish()

public function publish(bool $visible = true): self

Publish (or withdraw) it: on the applications page and downloadable once published - Thread::isVisible() then says so, drafts staying visible to their owners and the admins.

Software::getCategory()

public function getCategory(): string

Software::setCategory()

public function setCategory(string $category): self

Software::getStatus()

public function getStatus(): SoftwareStatus

Software::setStatus()

public function setStatus(SoftwareStatus $status): self

Software::getKind()

public function getKind(): SoftwareKind

Software::setKind()

public function setKind(SoftwareKind $kind): self

Software::isGame()

public function isGame(): bool

Software::hasPlayablePreview()

public function hasPlayablePreview(): bool

Software::getPlayablePreview()

public function getPlayablePreview(): bool

Software::setPlayablePreview()

public function setPlayablePreview(?bool $preview): self

Software::isPlayableInBrowser()

public function isPlayableInBrowser(): bool

Whether its WebGL build is played in its page: a free game always; a paid one only when the studio opened a playable preview of it - otherwise it is downloaded once bought.

Software::getPricing()

public function getPricing(): Pricing

Software::setPricing()

public function setPricing(Pricing $pricing): self

Software::isFree()

public function isFree(): bool

Software::getStack()

public function getStack(): array

Software::setStack()

public function setStack(array $stack): self

Software::getStackAsText()

public function getStackAsText(): string

The stack as the back office edits it: "Symfony, TransparentJS".

Software::setStackAsText()

public function setStackAsText(?string $stack): self

Software::getHomepage()

public function getHomepage(): ?string

Software::setHomepage()

public function setHomepage(?string $homepage): self

Software::getSourceUrl()

public function getSourceUrl(): ?string

Software::setSourceUrl()

public function setSourceUrl(?string $sourceUrl): self

Software::getRepositoryUrl()

public function getRepositoryUrl(): ?string

Software::setRepositoryUrl()

public function setRepositoryUrl(?string $repositoryUrl): self

Software::getGitRepository()

public function getGitRepository(): ?string

Software::setGitRepository()

public function setGitRepository(?string $gitRepository): self

Software::getPackageName()

public function getPackageName(): ?string

Software::setPackageName()

public function setPackageName(?string $packageName): self

Software::getUpmName()

public function getUpmName(): ?string

Software::setUpmName()

public function setUpmName(?string $upmName): self

Software::getImage()

public function getImage(): ?string

The picture's public path (an upload), or the stored path or URL as it is.

Software::getImageFile()

public function getImageFile()

Software::setImage()

public function setImage($image): self

Software::getScreenshots()

public function getScreenshots(): array
Returns

the screenshots' public paths, in their order

Software::getScreenshotFiles()

public function getScreenshotFiles()

Software::setScreenshots()

public function setScreenshots($screenshots): self

Software::getTrailer()

public function getTrailer(): ?string

The trailer's public path (an upload), or the stored URL as it is.

Software::getTrailerFile()

public function getTrailerFile()

Software::setTrailer()

public function setTrailer($trailer): self

Software::getDemoUrl()

public function getDemoUrl(): ?string

Software::setDemoUrl()

public function setDemoUrl(?string $demoUrl): self

Software::getPosition()

public function getPosition(): int

Software::setPosition()

public function setPosition(int $position): self

Software::getYear()

public function getYear(): ?string

Software::setYear()

public function setYear(?string $year): self

Software::getReleases()

public function getReleases(): Collection
Returns

newest first

Software::addRelease()

public function addRelease(Release $release): self

Software::removeRelease()

public function removeRelease(Release $release): self

A release never saved, taken back (its build failed): the next flush must not cascade it in.

Software::getLatestRelease()

public function getLatestRelease(): ?Release

Software::findRelease()

public function findRelease(string $version): ?Release

Software::getOffers()

public function getOffers(): Collection

Stage class

class Stage implements Stringable Stage.php:11

A step of a pipeline ("test", "build", "deploy"): the jobs that run together, after the stage before.

Stage::__construct()

public function __construct(string $name = '', int $position = 0)

Stage::__toString()

public function __toString(): string

Stage::getId()

public function getId(): ?int

Stage::getPipeline()

public function getPipeline(): ?Pipeline

Stage::setPipeline()

public function setPipeline(?Pipeline $pipeline): self

Stage::getName()

public function getName(): string

Stage::setName()

public function setName(string $name): self

Stage::getPosition()

public function getPosition(): int

Stage::setPosition()

public function setPosition(int $position): self

Stage::getJobs()

public function getJobs(): Collection

Stage::job()

public function job(string $name, PipelineStatus $status = PipelineStatus::PENDING): Job

The job of that name, made when there is none.

Stage::getStatus()

public function getStatus(): PipelineStatus

What its jobs say; one that failed and was allowed to is a warning.

TimeEntry class

class TimeEntry implements Stringable TimeEntry.php:15

Time the studio spent on a project. Logging it takes the same minutes off the client's support hours (Forge\Service\HourLedger, through TimeEntrySubscriber), so the balance on their dashboard is always the sum of what they bought and what was done.

TimeEntry::__construct()

public function __construct(int $minutes = 0, string $note = '', ?\DateTimeImmutable $spentOn = null)

TimeEntry::__toString()

public function __toString(): string

TimeEntry::formatMinutes()

public static function formatMinutes(int $minutes): string

TimeEntry::getId()

public function getId(): ?int

TimeEntry::getProject()

public function getProject(): ?Project

TimeEntry::setProject()

public function setProject(?Project $project): self

TimeEntry::getAuthor()

public function getAuthor(): ?User

TimeEntry::setAuthor()

public function setAuthor(?User $author): self

TimeEntry::getMinutes()

public function getMinutes(): int

TimeEntry::setMinutes()

public function setMinutes(int $minutes): self

TimeEntry::getNote()

public function getNote(): string

TimeEntry::setNote()

public function setNote(string $note): self

TimeEntry::getCommitSha()

public function getCommitSha(): ?string

TimeEntry::setCommitSha()

public function setCommitSha(?string $commitSha): self

TimeEntry::getSpentOn()

public function getSpentOn(): \DateTimeImmutable

TimeEntry::setSpentOn()

public function setSpentOn(\DateTimeImmutable $spentOn): self

TimeEntry::isBillable()

public function isBillable(): bool

TimeEntry::setBillable()

public function setBillable(bool $billable): self

Base\Forge\Entity\Product

HourPack class

class HourPack extends Product HourPack.php:14

Support hours, sold like any product of base-bundle-market: paying for n of them credits n × minutes to the buyer's ledger (OrderPaidSubscriber).

An accepted quote becomes a one-off, unlisted HourPack of its own.

HourPack::isShippable()

public function isShippable(): bool

Time, not goods: checkout asks for no address.

HourPack::getMaxQuantity()

public function getMaxQuantity(): ?int

A quote's own pack is bought once; listed packs, as many as wanted.

HourPack::getMinutes()

public function getMinutes(): int

HourPack::setMinutes()

public function setMinutes(int $minutes): self

HourPack::getHours()

public function getHours(): float

HourPack::setHours()

public function setHours(float $hours): self

HourPack::isListed()

public function isListed(): bool

HourPack::setListed()

public function setListed(bool $listed): self

HourPack::getHourlyPrice()

public function getHourlyPrice(): int

In cents: what one hour of this pack costs.

LicenseOffer class

class LicenseOffer extends Product LicenseOffer.php:14

A licence for a piece of software, sold through base-bundle-market: paying for it issues a Forge\Entity\License per unit (OrderPaidSubscriber).

LicenseOffer::isShippable()

public function isShippable(): bool

A key and downloads, nothing posted: checkout asks for no address.

LicenseOffer::getSoftware()

public function getSoftware(): ?Software

LicenseOffer::setSoftware()

public function setSoftware(?Software $software): self

LicenseOffer::getDurationMonths()

public function getDurationMonths(): ?int

LicenseOffer::setDurationMonths()

public function setDurationMonths(?int $durationMonths): self

LicenseOffer::getUpdatesMonths()

public function getUpdatesMonths(): ?int

LicenseOffer::setUpdatesMonths()

public function setUpdatesMonths(?int $updatesMonths): self

LicenseOffer::getMachinesPerSeat()

public function getMachinesPerSeat(): int

LicenseOffer::setMachinesPerSeat()

public function setMachinesPerSeat(?int $machines): self

LicenseOffer::getSeats()

public function getSeats(): int

LicenseOffer::setSeats()

public function setSeats(int $seats): self

Base\Forge\Enum

CardColumn enum

enum CardColumn CardColumn.php:6

The four columns of a project's board, in the order they are shown.

case BACKLOG = 'backlog'
case PROGRESS = 'progress'
case REVIEW = 'review'
case DONE = 'done'

CreditReason enum

enum CreditReason CreditReason.php:6

Why a client's support-hour balance moved.

case PURCHASE = 'purchase'
case TIME = 'time'
case ADJUSTMENT = 'adjustment'
case REFUND = 'refund'

LicenseStatus enum

enum LicenseStatus LicenseStatus.php:5

case ACTIVE = 'active'
case EXPIRED = 'expired'
case REVOKED = 'revoked'

PipelineStatus enum

enum PipelineStatus PipelineStatus.php:9

Where a pipeline, or one of its jobs, stands. The values are the states @glitchr/graphjs draws (its pipeline preset): a status is given to a node as it is.

case PENDING = 'pending'
case RUNNING = 'running'
case SUCCESS = 'success'
case WARNING = 'warning'
case FAILED = 'failed'
case CANCELED = 'canceled'
case SKIPPED = 'skipped'
case MANUAL = 'manual'

PipelineStatus::isFinished()

public function isFinished(): bool

Nothing more will happen by itself.

PipelineStatus::of()

public static function of(iterable $parts): self

The status of a whole from its parts' (a stage from its jobs, a pipeline from its stages): failed as soon as one failed, running while one runs, waiting on a manual step, pending while one has not started; then success - with a warning when a job allowed to fail did.

Platform enum

enum Platform Platform.php:11

A platform a game or an application is built for. Its build is a release artifact whose format is the platform's value: a zip of the build folder (webgl, windows, macos, linux) or the package itself (android: an .apk or an .aab).

case WEBGL = 'webgl'
case WINDOWS = 'windows'
case MACOS = 'macos'
case LINUX = 'linux'
case ANDROID = 'android'

Platform::label()

public function label(): string

Platform::icon()

public function icon(): string

Platform::extensions()

public function extensions(): array
Returns

the file extensions its build may come as

Platform::values()

public static function values(): array

Pricing enum

enum Pricing Pricing.php:6

Whether a piece of software is downloaded freely or against a licence.

case FREE = 'free'
case LICENSED = 'licensed'

Pricing::isFree()

public function isFree(): bool

ProjectStatus enum

enum ProjectStatus ProjectStatus.php:5

case OPEN = 'open'
case PAUSED = 'paused'
case DONE = 'done'

SoftwareKind enum

enum SoftwareKind SoftwareKind.php:11

What a piece of software is delivered as: a package a project installs (Composer, Unity's Package Manager), a game people play (in the page when it has a WebGL build, else downloaded per platform), or an application people install.

case PACKAGE = 'package'
case GAME = 'game'
case APP = 'app'

SoftwareStatus enum

enum SoftwareStatus SoftwareStatus.php:5

case LIVE = 'live'
case DEV = 'dev'
case DRAFT = 'draft'

Base\Forge\EventListener

LicenseSeatListener class

final class LicenseSeatListener LicenseSeatListener.php:15

A seat saved from the back office tells its holder, as one given from the owner's account does (LicenseSeats::give, which sends it itself: the notified flag keeps it to one e-mail).

LicenseSeatListener::__construct()

public function __construct(readonly LicenseSeats $seats)

LicenseSeatListener::postPersist()

public function postPersist(LicenseSeat $seat): void

Base\Forge\EventSubscriber

LicenseApiErrorSubscriber class

final class LicenseApiErrorSubscriber implements EventSubscriberInterface LicenseApiErrorSubscriber.php:18

The licence API answers its errors in JSON, with their own status - a request it cannot read is a 422 with what is wrong -, never the site's HTML error page: the one reading it is a game or an application.

Before base-bundle's error pages, which would pick their own status.

LicenseApiErrorSubscriber::getSubscribedEvents()

public static function getSubscribedEvents(): array

LicenseApiErrorSubscriber::onException()

public function onException(ExceptionEvent $event): void

OrderPaidSubscriber class

final class OrderPaidSubscriber OrderPaidSubscriber.php:32

What a paid order delivers, since nothing here ships:

HourPack its minutes × quantity on the buyer's ledger LicenseOffer one licence per unit, for the offer's software a quote's the quote marked paid (its pack is an HourPack: the own pack hours come with it)

Checkout::confirm() dispatches OrderPaidEvent once, but a payment provider's webhook and its redirect may both get there: everything is skipped when this order's reference was already delivered.

OrderPaidSubscriber::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly HourLedger $ledger, readonly HourCreditRepository $credits, readonly LicenseIssuer $issuer, readonly ?LoggerInterface $logger = null)

OrderPaidSubscriber::__invoke()

public function __invoke(OrderPaidEvent $event): void

TimeEntryLedgerSubscriber class

final class TimeEntryLedgerSubscriber TimeEntryLedgerSubscriber.php:18

Keeps every time entry's debit line in step with it, whoever writes the entry (the back office, a command, a fixture): logged, changed or made non-billable, the client's balance follows in the same flush. A deleted entry takes its line with it (ON DELETE CASCADE).

TimeEntryLedgerSubscriber::__construct()

public function __construct(readonly HourLedger $ledger)

TimeEntryLedgerSubscriber::onFlush()

public function onFlush(OnFlushEventArgs $args): void

Base\Forge\Exception

HourRefundException class

final class HourRefundException extends RuntimeException HourRefundException.php:6

A refund HourRefunds would not make, or Stripe refused; nothing was changed.

LicenseActivationException class

final class LicenseActivationException extends RuntimeException LicenseActivationException.php:6

Why an activation is refused: a reason the software can act on, and its HTTP status.

const INVALID_KEY = 'invalid_key'
const WRONG_SOFTWARE = 'wrong_software'
const NOT_VALID = 'license_not_valid'
const NO_MACHINE_LEFT = 'no_machine_left'
const NOT_CONFIGURED = 'not_configured'
public readonly string $reason
public readonly array $details

LicenseActivationException::__construct()

public function __construct(public readonly string $reason, public readonly array $details = [])

LicenseActivationException::getStatus()

public function getStatus(): int

Base\Forge\Form

QuoteLineType class

class QuoteLineType extends AbstractType QuoteLineType.php:15

One line of a quote in the back office: the work, its hours, the rate per hour (cents).

QuoteLineType::__construct()

public function __construct(readonly int $hourlyRate = 6000)

QuoteLineType::buildForm()

public function buildForm(FormBuilderInterface $builder, array $options): void

The texts name their domain (@forge.…): base-bundle gives every field the "fields" domain, which the form's own translation_domain does not reach.

QuoteLineType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

ReviewAnswerType class

class ReviewAnswerType extends AbstractType ReviewAnswerType.php:18

The review form: five stars (radios, drawn as stars), a few words, how to sign. The texts name their domain (@forge.…): base-bundle gives every field the "fields" domain.

ReviewAnswerType::buildForm()

public function buildForm(FormBuilderInterface $builder, array $options): void

ReviewAnswerType::configureOptions()

public function configureOptions(OptionsResolver $resolver): void

Base\Forge\Git

ForgeRepositoryProvider class

final class ForgeRepositoryProvider implements RepositoryProviderInterface ForgeRepositoryProvider.php:17

Hands git/git-bundle the repositories of the projects and the software, so a new client project is browsable (and clonable by git:sync) the moment it is saved - no git.repositories line to add. Each lives at forge.repositories_dir/<repository>.git; one also declared in git.repositories keeps its configured path.

ForgeRepositoryProvider::__construct()

public function __construct(readonly ProjectRepository $projects, readonly SoftwareRepository $software, readonly string $directory)

ForgeRepositoryProvider::getRepositories()

public function getRepositories(): array

Base\Forge\Model

ActivationRequest class

final class ActivationRequest ActivationRequest.php:8

What a piece of software sends to activate - or re-activate - its machine.

public readonly string $key

The seat's licence key: glk_…

public readonly string $software

The software's slug on the forge: a key only opens its own software.

public readonly string $machine

A stable identifier of the machine (Unity: SystemInfo.deviceUniqueIdentifier; macOS: IOPlatformUUID). Hashed, never stored as is.

public readonly ?string $name

What the machine is called, to recognise it by: "MacBook Air de Camille".

public readonly ?string $platform

macos, windows, linux, android, ios.

..

public readonly ?string $version

The software's own version.

ActivationRequest::__construct()

public function __construct(public readonly string $key = '', public readonly string $software = '', public readonly string $machine = '', public readonly ?string $name = null, public readonly ?string $platform = null, public readonly ?string $version = null)

CardMove class

final class CardMove CardMove.php:9

A drop on a project's board: which column, at which position (BoardController, #[MapRequestPayload]).

public readonly string $column
public readonly int $position

CardMove::__construct()

public function __construct(public readonly string $column = '', public readonly int $position = 0)

CardMove::columns()

public static function columns(): array

CardMove::getColumn()

public function getColumn(): CardColumn

ReleaseRequest class

final class ReleaseRequest ReleaseRequest.php:8

What a piece of software sends to free its own machine's place.

public readonly string $key
public readonly string $machine

ReleaseRequest::__construct()

public function __construct(public readonly string $key = '', public readonly string $machine = '')

ReviewAnswer class

final class ReviewAnswer ReviewAnswer.php:8

What the client answers about their project.

public ?int $rating = null
public ?string $comment = null
public ?string $signature = null

Base\Forge\Repository

HourCreditRepository class

class HourCreditRepository extends ServiceEntityRepository HourCreditRepository.php:11

HourCreditRepository::__construct()

public function __construct(ManagerRegistry $registry)

HourCreditRepository::balance()

public function balance(User $user): int

Minutes left: everything bought minus everything logged.

HourCreditRepository::history()

public function history(User $user, int $limit = 50): array
Returns

newest first

HourCreditRepository::lowBalances()

public function lowBalances(int $minutes): array
Returns

user id => balance, for the users below $minutes who once bought hours

HourCreditRepository::hasOrder()

public function hasOrder(string $reference): bool

LicenseRepository class

class LicenseRepository extends ServiceEntityRepository LicenseRepository.php:14

LicenseRepository::__construct()

public function __construct(ManagerRegistry $registry)

LicenseRepository::findOwnedBy()

public function findOwnedBy(User $user): array
Returns

newest first

LicenseRepository::findHeldBy()

public function findHeldBy(User $user, ?Software $software = null): array
Returns

the licences a seat of which that user holds - linked to them, or given to their e-mail once verified

LicenseRepository::findValidFor()

public function findValidFor(User $user, Software $software): array
Returns

the user's valid licences for that software - a seat of which they hold -, best first

LicenseRepository::findSeatByKey()

public function findSeatByKey(string $key): ?LicenseSeat

The seat a key was made for.

LicenseRepository::findValidByEmail()

public function findValidByEmail(string $email): array
Returns

the valid licences a seat of which is given to that e-mail

LicenseRepository::findLapsed()

public function findLapsed(\DateTimeImmutable $at): array
Returns

active ones whose expiry has passed

PipelineRepository class

class PipelineRepository extends ServiceEntityRepository PipelineRepository.php:11

PipelineRepository::__construct()

public function __construct(ManagerRegistry $registry)

PipelineRepository::findLatestFor()

public function findLatestFor(Project $project, int $limit = 5): array
Returns

a project's latest runs, newest first

PipelineRepository::findLatest()

public function findLatest(int $limit = 30): array
Returns

the latest runs of every project, newest first

PipelineRepository::findOneBySource()

public function findOneBySource(string $source, string $externalId): ?Pipeline

The run a source calls by that id: the one to bring up to date rather than record twice.

ProjectRepository class

class ProjectRepository extends ServiceEntityRepository ProjectRepository.php:11

ProjectRepository::__construct()

public function __construct(ManagerRegistry $registry)

ProjectRepository::findForClient()

public function findForClient(User $client): array
Returns

open ones first, then by name

ProjectRepository::findWithRepository()

public function findWithRepository(): array
Returns

every project with a git repository

ProjectRepository::findOneByRepository()

public function findOneByRepository(string $repository): ?Project

ProjectReviewRepository class

class ProjectReviewRepository extends ServiceEntityRepository ProjectReviewRepository.php:10

ProjectReviewRepository::__construct()

public function __construct(ManagerRegistry $registry)

ProjectReviewRepository::findPublished()

public function findPublished(int $limit = 12): array
Returns

the ones the studio shows, newest first

QuoteRepository class

class QuoteRepository extends ServiceEntityRepository QuoteRepository.php:17

The studio's quotes, numbered Q-<year>-<n>: what every quote table answers is omnibase/marketplace's (QuoteRepositoryTrait: saveNumbered(), findForClient(), pipeline()).

QuoteRepository::__construct()

public function __construct(ManagerRegistry $registry)

SoftwareRepository class

class SoftwareRepository extends ServiceEntityRepository SoftwareRepository.php:12

SoftwareRepository::__construct()

public function __construct(ManagerRegistry $registry)

SoftwareRepository::findVisible()

public function findVisible(): array
Returns

the published ones, in the applications page's order

SoftwareRepository::findDownloadable()

public function findDownloadable(): array
Returns

the ones with something to download

SoftwareRepository::findWithRepository()

public function findWithRepository(): array
Returns

the ones git/git-bundle can build releases for

SoftwareRepository::findPackages()

public function findPackages(): array
Returns

PHP packages served by the Composer repository

SoftwareRepository::findUpmPackages()

public function findUpmPackages(): array
Returns

Unity packages served by the Unity registry

SoftwareRepository::findGames()

public function findGames(): array
Returns

the published games, in the applications page's order

Base\Forge\Security\Voter

ArtifactVoter class

final class ArtifactVoter extends Voter ArtifactVoter.php:19

Who may download an artifact: anyone when its software is free; else the holder of a valid licence covering its release - and the staff.

const DOWNLOAD = 'FORGE_DOWNLOAD'

ArtifactVoter::__construct()

public function __construct(readonly LicenseRepository $licenses, readonly Security $security)

GitRepositoryVoter class

final class GitRepositoryVoter extends Voter GitRepositoryVoter.php:23

git/git-bundle's per-repository check (git.repository_attribute: GIT_VIEW): the staff browse everything; a client their projects' repositories; the source of a free piece of software is open to any signed-in visitor, that of a licensed one to its licence holders. A repository nothing here claims (declared only in git.repositories) stays the staff's.

const VIEW = 'GIT_VIEW'

GitRepositoryVoter::__construct()

public function __construct(readonly ProjectRepository $projects, readonly SoftwareRepository $software, readonly LicenseRepository $licenses, readonly Security $security)

ProjectVoter class

final class ProjectVoter extends Voter ProjectVoter.php:19

A project is its client's, and the studio's: the client sees it and moves its cards - until it is delivered (closed); staff see and move every one.

const VIEW = 'FORGE_PROJECT_VIEW'
const EDIT_BOARD = 'FORGE_PROJECT_EDIT_BOARD'

ProjectVoter::__construct()

public function __construct(readonly Security $security)

Base\Forge\Service

ArtifactBuilder class

class ArtifactBuilder ArtifactBuilder.php:25

Builds a release's zip straight from its git tag - every file of the tree, read by git/git-bundle from the object database, under a <software>-<version>/ folder. Files marked export-ignore are not filtered (there is no .gitattributes evaluation here): keep the tree clean.

For a Unity package (Software::$upmName) the same tree is also written as the tarball npm and Unity's Package Manager expect (format "upm"): a gzip'd ustar archive, every file under package/, its package.json naming the package and the release's version. A game's or an application's builds are made elsewhere (Unity, a CI) and attached to the release per platform (attach()).

ArtifactBuilder::__construct()

public function __construct(readonly Git2Service $git, readonly string $storageDir, readonly Filesystem $filesystem = new Filesystem())

ArtifactBuilder::build()

public function build(Release $release): Artifact

ArtifactBuilder::buildUpm()

public function buildUpm(Release $release): Artifact

The release's UPM tarball, <upm name>-<version>.tgz, from the tree at its tag. Not flushed.

ArtifactBuilder::attach()

public function attach(Release $release, Platform $platform, string $source, ?string $filename = null): Artifact

A build made elsewhere (Unity, a CI) becomes the release's artifact for that platform: copied into the storage, its sha256 taken, the previous build for that platform replaced. Not flushed.

ArtifactBuilder::isWebglBuild()

public static function isWebglBuild(string $zipFile): bool

A zip with an index.html and a Build/ folder at its root, or under one folder.

ArtifactBuilder::webglRoot()

public static function webglRoot(\ZipArchive $zip): ?string

The folder of a WebGL build's zip that holds its index.html and Build/ ("" at the root), or null.

ArtifactBuilder::absolutePath()

public function absolutePath(Artifact $artifact): string

ArtifactBuilder::tar()

public static function tar(array $files, int $mtime = 0): string

A ustar archive of those files (path => [content, mode]), every entry dated $mtime: the same tree makes the same bytes, so the same shasum.

ArtifactFiles class

class ArtifactFiles ArtifactFiles.php:26

An artifact's file handed out, once whoever asks was let through: one Download row for each new download (not a HEAD, not the rest of a download resumed), then the file - by nginx (X-Accel-Redirect to forge.accel_prefix) or, without it, by PHP. The signed links (DownloadController) and the Unity registry's tarballs (UpmController) both end here.

ArtifactFiles::__construct()

public function __construct(readonly ArtifactBuilder $builder, readonly EntityManagerInterface $entityManager, readonly string $secret, readonly ?string $accelPrefix = null)

ArtifactFiles::respond()

public function respond(Request $request, Artifact $artifact, ?License $license, mixed $user, string $channel): Response
$channel

web, composer or upm: where the download came from

Board class

final class Board Board.php:14

A project's board, as it is moved: a card put at a place of a column, and the columns it touched renumbered from zero - so the positions always read 0, 1, 2... with no gap and no tie, whatever was dragged where. Nothing is saved here: the caller flushes.

Board::move()

public function move(Card $card, CardColumn $column, int $position): void

Put the card at $position of $column (past the end: last) and renumber the column it arrives in, and the one it left when it changed.

Board::add()

public function add(Project $project, Card $card): Card

A new card at the end of its column.

ComposerIndex class

class ComposerIndex ComposerIndex.php:26

The studio's own Packagist: the index (packages.json) of a Composer repository of the PHP packages among the software. Free ones are listed for anyone; licensed ones for the holder of a valid licence, each version its licence covers, with a dist URL signed for that licence.

composer config repositories.glitchr composer https://glitchr.io/composer composer config http-basic.glitchr.io <email> <licence key> composer require glitchr/<package>

Each version's metadata is its own composer.json at that tag, read through git/git-bundle, with name, version, dist and source set here.

ComposerIndex::__construct()

public function __construct(readonly SoftwareRepository $software, readonly LicenseHolders $holders, readonly DownloadLinks $links, readonly Git2Service $git, readonly ?LoggerInterface $logger = null)

ComposerIndex::packages()

public function packages(array $licenses = []): array
$licenses

the caller's valid licences (empty: anonymous)

ComposerIndex::authenticate()

public function authenticate(?string $email, ?string $key): array

Who is behind a Basic auth pair (e-mail, key): the seat that key was made for, given to that e-mail, on a valid licence - and then every valid licence with a seat for that e-mail.

Returns

the e-mail, or null when the pair is wrong

class DeviceLinks DeviceLinks.php:19

An editor linked to its user's licences, by a device code (as OAuth's device flow): the Unity window asks for a code, the user opens the link page on the site, signs in, buys what they need, and links the editor; the window, which asked meanwhile, then receives a licence key - the key of one of the user's seats, made anew (the seat's previous key stops working) - and stores it as the registry's token in the user's .upmconfig.toml.

The codes live ten minutes in the cache, nowhere else; a key is handed over once.

public function __construct(readonly CacheItemPoolInterface $cache, readonly LicenseRepository $licenses, readonly LicenseIssuer $issuer)
public function start(?string $item = null): array
public function find(string $userCode): ?array
Returns

the code a user typed, while it waits

public function approve(string $userCode, User $user): ?LicenseSeat

The user links the editor: a seat of theirs on a valid licence gets a new key, kept for the editor to take. Null when they hold no seat to give it.

public function poll(string $deviceCode): array

What the editor gets when it asks: the key once approved (then the code is spent), else why not - authorization_pending, slow_down, expired_token (RFC 8628's words).

public function seatOf(User $user): ?LicenseSeat

A seat the user holds, on a valid licence: theirs first (given to their account), the one with a key first.

HourLedger class

class HourLedger HourLedger.php:16

A client's support hours: credited when they buy some, debited when time is logged on their projects. The balance is always the sum of the lines.

HourLedger::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly HourCreditRepository $credits)

HourLedger::balance()

public function balance(User $user): int

HourLedger::history()

public function history(User $user, int $limit = 50): array

HourLedger::credit()

public function credit(User $user, int $minutes, CreditReason $reason = CreditReason::PURCHASE, ?string $note = null, ?string $orderReference = null): HourCredit

Persisted, not flushed.

HourLedger::debitFor()

public function debitFor(TimeEntry $entry, ?HourCredit $existing): ?HourCredit

The debit line a time entry should have: its minutes, negated, on the project's client - or none, when it is not billable. Existing lines are updated in place (TimeEntryLedgerSubscriber calls this in onFlush).

HourRefunds class

class HourRefunds HourRefunds.php:34

Paying hours back. The studio keeps the right to refund hours a client bought - when the work does not sit with their values, or the studio's - with an explanation, always. What is refundable is what the order paid, less what was already refunded; what it suggests is the share of the hours still unused (the ledger is one pool: the order's hours, capped by the balance left).

Paid online (through one of glitchr/omnitrade's gateways - Stripe, PayPal... - the marketplace's bridge keeps the provider's reference on the payment), the amount goes back through that provider; otherwise (bank transfer) it is recorded, to be wired back by hand. Then the hours leave the ledger, the order is marked refunded once fully paid back, and the client gets the explanation by e-mail.

HourRefunds::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly HourLedger $ledger, readonly PaymentGatewayRegistry $gateways, readonly MailerInterface $mailer)

HourRefunds::order()

public function order(string $reference): ?Order

HourRefunds::summary()

public function summary(Order $order): array

What this order can still give back.

HourRefunds::refunds()

public function refunds(string $reference): array

HourRefunds::refund()

public function refund(Order $order, int $amount, int $minutes, string $reason, ?User $by = null): HourRefund

Refunds $amount cents and takes $minutes back from the ledger, for $reason (sent to the client). One at a time per order: the order's row is locked while it runs, and Stripe gets an idempotency key.

HubCatalog class

class HubCatalog HubCatalog.php:26

What the hub clients read - the Unity window (com.glitchr.hub) and the desktop hub: the hub manifest (/upm/-/hub.json: the brand, the sources, whether and how to buy) and this forge's catalogue (/upm/-/hub/catalog.json: its Unity packages, free and paid, with their versions, changelogs, pictures and offers, and its games). The Unity registry itself (UpmRegistry) stays what Unity's Package Manager reads; the catalogue adds what a shop window shows.

Its games follow the shelf's rule: a free game is played in the browser and downloaded; a paid one is downloaded once bought, played in the browser only when its playable preview is open (Software::isPlayableInBrowser()).

HubCatalog::__construct()

public function __construct(readonly SoftwareRepository $software, readonly UpmRegistry $registry, readonly UrlGeneratorInterface $urls, readonly UrlHelper $urlHelper, readonly array $hub = [])

HubCatalog::manifest()

public function manifest(): array

The hub manifest: version 1. A source without "checkout" offers nothing to buy.

HubCatalog::catalog()

public function catalog(array $licenses = []): array

The catalogue, as the caller sees it: every Unity package - a paid one listed with its offers, "installable" when the caller's licences cover it - and every published game.

$licenses

the caller's valid licences (the Bearer key of the request)

HubCatalog::licenses()

public function licenses(array $licenses, string $email): array

The licences of a key's holder, as a hub client shows them: each one's software (its slug, which the licence API asks for), validity, seats, and the machines of that holder's seat - each with the SHA-256 of its identifier, which the client compares with its own.

LicenseActivations class

class LicenseActivations LicenseActivations.php:20

The machines a seat's software runs on. The software sends its seat's key and its machine's identifier: the first time, the machine takes one of the seat's places (License::$machinesPerSeat); every time, it gets a fresh signed token (LicenseToken) to run offline until the next. When every place is taken it is told which machines hold them - its holder frees one from their account, or from that machine (release()).

LicenseActivations::__construct()

public function __construct(readonly LicenseRepository $licenses, readonly LicenseToken $tokens, readonly EntityManagerInterface $entityManager)

LicenseActivations::activate()

public function activate(string $key, string $machineId, string $software, ?string $name = null, ?string $platform = null, ?string $version = null): array

Activates the machine - or finds it already activated - and signs it a token. Flushed.

Throws

LicenseActivationException

LicenseActivations::releaseMachine()

public function releaseMachine(string $key, string $machineId): bool

The software frees its own machine - before an uninstall, a move.

Flushed. False when that machine was not activated.

Throws

LicenseActivationException a key that is no seat's

LicenseActivations::release()

public function release(LicenseActivation $activation): void

Frees the machine's place: its token runs until it expires, then it must ask again. Flushed.

LicenseHolders class

class LicenseHolders LicenseHolders.php:15

Who is behind a licence key a package manager sends: the seat that key was made for, on a valid licence - and then every valid licence with a seat for that seat's e-mail. Composer sends the e-mail with the key (HTTP Basic, composer config http-basic); Unity's Package Manager sends the key alone (token in .upmconfig.toml: a Bearer) or both (_auth).

LicenseHolders::__construct()

public function __construct(readonly LicenseRepository $licenses)

LicenseHolders::authenticate()

public function authenticate(?string $key, ?string $email = null, bool $emailRequired = false): array
$email

when given, the seat's own: a key with another e-mail is refused

Returns

the e-mail, or null when the key (or the pair) is wrong

LicenseHolders::fromHeader()

public static function fromHeader(?string $authorization): ?array

The credentials of an Authorization header: "Bearer <key>", or "Basic base64(<e-mail>:<key>)".

Returns

the key and the e-mail (null: none sent), or null when there is no such header

LicenseIssuer class

class LicenseIssuer LicenseIssuer.php:13

Issues licences (from a paid offer, or by hand) and the keys of their seats.

LicenseIssuer::__construct()

public function __construct(readonly EntityManagerInterface $entityManager)

LicenseIssuer::issue()

public function issue(Software $software, User $owner, ?LicenseOffer $offer = null, ?string $orderReference = null, ?\DateTimeImmutable $at = null): License

A licence as the offer describes it: its seats, its validity, its updates - its first seat its owner's own, to give away if they buy it for someone else. Persisted, not flushed.

LicenseIssuer::rekey()

public function rekey(LicenseSeat $seat): string

A new key for the seat, replacing the old one (which stops working at once). Returned in clear this once; only its hash is kept. Flushed.

LicenseKey class

final class LicenseKey LicenseKey.php:10

Licence keys: "glk_" and 32 characters of Crockford base32 (160 bits), grouped by four for reading aloud. The prefix makes a leaked key easy to recognise, and the Composer repository accepts it as the password.

const PREFIX = 'glk_'

LicenseKey::generate()

public static function generate(): string

LicenseKey::looksValid()

public static function looksValid(string $key): bool

LicenseSeats class

class LicenseSeats LicenseSeats.php:20

Gives a licence's seats to e-mails, and takes them back. The one it is given to gets an e-mail: what they may now download, and how - sign in, or create their account, with that address.

LicenseSeats::__construct()

public function __construct(readonly EntityManagerInterface $entityManager, readonly MailerInterface $mailer, readonly ?LoggerInterface $logger = null)

LicenseSeats::give()

public function give(License $license, string $email, ?User $by = null): LicenseSeat

A seat for that e-mail - or the one it already has. Flushed, and the e-mail sent (a mail that fails does not undo the seat).

Throws

InvalidArgumentException not an e-mail, or no seat left

LicenseSeats::takeBack()

public function takeBack(LicenseSeat $seat): void

Takes the seat back: its key stops working at once. Flushed.

LicenseSeats::welcome()

public function welcome(LicenseSeat $seat): void

A seat given from the back office: its holder told. Its account needs no link - a verified account with that e-mail holds it anyway.

LicenseToken class

class LicenseToken LicenseToken.php:32

The proof an activated machine keeps, to run without asking again: what the licence allows, for that machine, until when - signed with the studio's Ed25519 key, so the software checks it offline with the public key it ships with, and nothing it holds can forge one.

token = base64url(payload JSON) "." base64url(Ed25519 signature of the payload JSON)

The payload: v 1 license the licence's id software the software's slug - a token for another is refused email the seat's e-mail machine SHA-256 of the machine identifier - the software compares it to its own issued_at UNIX time valid_until UNIX time: past it, the software asks again (FORGE_LICENSE_GRACE_DAYS, never beyond the licence's own expiry) license_expires UNIX time, or null: perpetual updates_until UNIX time, or null: the releases published until then are covered

The secret key is FORGE_LICENSE_SIGNING_KEY (base64 of sodium's 64-byte secret key; bin/console forge:license:keypair makes a pair): a secret, in the vault, never in the repository.

LicenseToken::__construct()

public function __construct(readonly ?string $secretKey = null, readonly ?string $graceDays = null)

LicenseToken::isConfigured()

public function isConfigured(): bool

LicenseToken::publicKey()

public function publicKey(): ?string

The public key the software ships with, base64.

LicenseToken::issue()

public function issue(LicenseActivation $activation, ?\DateTimeImmutable $at = null): array

LicenseToken::verify()

public static function verify(string $token, string $publicKey): ?array

What the software does, offline: the payload when the signature is the studio's, else null. Here for the tests and for tools written in PHP.

LicenseToken::keypair()

public static function keypair(): array
Returns

a new pair, base64

PipelineGraph class

final class PipelineGraph PipelineGraph.php:14

A pipeline as a graph: a node per job, ranked by its stage, in the state of its status; an edge from each job it waits for - the ones it names (needs), or every job of the stage before.

The shape is the one @glitchr/graphjs draws (its pipeline preset): given to setData() as it is, or written in the page as data-graph-* attributes (templates/_pipeline.html.twig).

PipelineGraph::data()

public function data(Pipeline $pipeline): array

PipelineGraph::sources()

public function sources(Pipeline $pipeline): array
Returns

for each job's node, the nodes it comes from (data-graph-from)

PipelineGraph::id()

public static function id(Job $job): string

A job's node: its id once saved, its place otherwise - never its name, which may hold anything.

ProjectReviews class

class ProjectReviews ProjectReviews.php:18

Asking a client what they think of their project: it is closed - delivered, its board no longer moves -, and they get an e-mail with their own link to the form: one to five stars, a few words.

ProjectReviews::__construct()

public function __construct(readonly ProjectReviewRepository $reviews, readonly EntityManagerInterface $entityManager, readonly MailerInterface $mailer)

ProjectReviews::request()

public function request(Project $project): ProjectReview

Closes the project and sends the client their link - a new one when asked again. Flushed.

Throws

InvalidArgumentException no client to ask, or already answered

QuoteStatusGuard class

final class QuoteStatusGuard QuoteStatusGuard.php:12

Small predicates on a quote's life: omnibase/marketplace's (Base\Marketplace\Service\QuoteStatusGuard), kept under this name for the code that uses it.

QuoteStatusGuard::isAwaitingPayment()

public static function isAwaitingPayment(Quote $quote): bool

Accepted, its pack made, not paid yet: going back to checkout is allowed.

QuoteToOrder class

class QuoteToOrder extends MarketplaceQuoteToOrder QuoteToOrder.php:27

The client accepts a quote: it becomes a one-off HourPack of the support store - its hours, its discounted total, one in stock - dropped in their cart. From there it is an order like any other; OrderPaidSubscriber credits the hours and marks the quote paid once it is.

omnibase/marketplace's QuoteToOrder, the studio's way: one HourPack rather than an order of the quote's lines.

QuoteToOrder::__construct()

public function __construct(EntityManagerInterface $entityManager, readonly Cart $cart, string $storeSlug = 'support')

QuoteToOrder::accept()

public function accept(AbstractQuote $quote, User $client): Order

ReleaseSync class

class ReleaseSync ReleaseSync.php:16

Tags become releases: for each software with a repository, every tag that reads as a version (v1.2.3, 1.2, 2.0.0-beta1) and has no release yet gets one - its commit, its message as changelog, its date as publication - and its zip, and its UPM tarball when the software is a Unity package.

const VERSION = '/^v?(\d+(?:\.\d+){0,3}(?:[-.]?(?:alpha|beta|rc|patch|p)\.?\d*)?)$/i'

ReleaseSync::__construct()

public function __construct(readonly Git2Service $git, readonly ArtifactBuilder $builder, readonly EntityManagerInterface $entityManager)

ReleaseSync::sync()

public function sync(Software $software, bool $build = true): array
Returns

the releases created, flushed

ReleaseSync::rebuild()

public function rebuild(Release $release): void

Rebuild one release's archives (a tag moved, the storage was lost). Flushed.

UpmRegistry class

class UpmRegistry UpmRegistry.php:39

The studio's Unity registry: the npm registry protocol Unity's Package Manager reads from a scoped registry, over the Unity packages among the software (Software::$upmName) - ComposerIndex's sibling. Free packages for anyone; licensed ones for the holder of a valid licence, each version its licence covers.

Packages/manifest.json "scopedRegistries": [{"name": "Glitch Art", "url": "https://glitchr.io/upm", "scopes": ["com.glitchr"]}] ~/.upmconfig.toml (a licensed package) [npmAuth."https://glitchr.io/upm"] token = "<licence key>" alwaysAuth = true

What the Package Manager asks, as seen from Unity 6 (2026-10-26): the search, GET /-/v1/search?text=<scope>&from=0&size=250, page after page until one comes back empty (GET /-/all when the search is a 404); the package document, GET /<name>; then HEAD and GET of the version's dist.tarball - every request with the token as "Authorization: Bearer" (or the Basic pair of _auth).

Each version's metadata is its package.json at that tag, read through git/git-bundle, with its name and version set here - as the tarball has them (ArtifactBuilder::buildUpm()).

UpmRegistry::__construct()

public function __construct(readonly SoftwareRepository $software, readonly LicenseHolders $holders, readonly UrlGeneratorInterface $urls, readonly Git2Service $git, readonly ?LoggerInterface $logger = null)

UpmRegistry::authenticate()

public function authenticate(?string $authorization): ?array

Who the Authorization header is: [null, []] without one (anonymous), null when it is wrong (a 401, so that Unity says so rather than resolving without the licensed packages).

UpmRegistry::find()

public function find(string $name): ?Software

UpmRegistry::document()

public function document(Software $software, array $licenses = []): array

The package document (npm's "packument"): the versions the caller may install, newest first in dist-tags.latest. Empty versions: none for them (the controller tells a 401 from a 403 from a 404).

$licenses

the caller's valid licences

public function search(string $text, int $from = 0, int $size = 20, array $licenses = []): array

The search: the packages whose name, display name, description or keywords hold every word of the text (Unity sends the scope), one page.

UpmRegistry::all()

public function all(array $licenses = []): array

The older listing of every package, which the Package Manager reads when the search is not there.

UpmRegistry::tarball()

public function tarball(Software $software, string $filename, array $licenses = []): ?array

The tarball of that file name, when the caller may have it.

UpmRegistry::release()

public function release(Software $software, string $filename): ?Release

The published release that tarball is of, whoever may have it (a 401/403 rather than a 404).

UpmRegistry::hasVersions()

public function hasVersions(Software $software): bool

Whether it has versions to install at all, for someone (a 401/403 rather than a 404).

WebglBuilds class

class WebglBuilds WebglBuilds.php:26

A game's WebGL build, played in its page: its zip unpacked once, beside it in the storage (<software>/<version>/webgl/play/), and each file served with the headers Unity's loader needs.

A build compressed with Brotli or gzip (Player Settings > Publishing Settings > Compression Format, Unity's default) names its files Build/<name>.wasm.br, .data.br, .framework.js.br: they are served as they are, with Content-Encoding: br (or gzip) and the type of what they hold - application/wasm for the .wasm, so that the browser compiles it as it streams in. The browser decompresses them, not Unity's JavaScript: no "Decompression Fallback" is needed, and none is used by the builds made for the forge. A build made with the fallback (its files end in .unityweb) is served too, without Content-Encoding: its loader decompresses it.

WebglBuilds::__construct()

public function __construct(readonly ArtifactBuilder $builder, readonly Filesystem $filesystem = new Filesystem())

WebglBuilds::file()

public function file(Artifact $build, string $path): ?array

The file at that path of the build, unpacked if it was not yet, with the headers to serve it with; null when the build has no such file.

Returns

its absolute path and its headers

WebglBuilds::unpack()

public function unpack(Artifact $build): string

Where the build is unpacked: made the first time it is asked for.

WebglBuilds::headers()

public static function headers(string $path): array

Base\Forge\Twig

ForgeExtension class

final class ForgeExtension ForgeExtension.php:14

{{ 210|forge_hours }} → 3h30; {{ 2500|forge_money('EUR') }} → 25,00 €; {{ 2500|forge_price('EUR') }} → 25,00 € ≈ 27,30 $US for a visitor who reads dollars (base-bundle's Trading rendered currency, with its rate).

ForgeExtension::__construct()

public function __construct(readonly ?TradingInterface $trading = null)

ForgeExtension::hours()

public function hours(?int $minutes): string

ForgeExtension::money()

public function money(?int $cents, string $currency = 'EUR', ?string $locale = null): string

ForgeExtension::price()

public function price(?int $cents, string $currency = 'EUR', ?string $locale = null): string

A price as it is charged, then what it comes to in the visitor's currency when that is another and a rate is known: indicative only, the payment stays in the price's own currency.

PipelineExtension class

final class PipelineExtension PipelineExtension.php:15

What the pipelines' templates ask: a project's latest runs, and a run as a graph (Base\Forge\Service\PipelineGraph).

PipelineExtension::__construct()

public function __construct(readonly PipelineRepository $pipelines, readonly PipelineGraph $graph)

PipelineExtension::pipelines()

public function pipelines(Project $project, int $limit = 5): array
Returns

newest first

PipelineExtension::graph()

public function graph(Pipeline $pipeline): array
Returns

for Graph.setData()

PipelineExtension::sources()

public function sources(Pipeline $pipeline): array
Returns

node id -> the nodes it comes from