Read from the package's own PHP files at the commit 200df927930c: its namespaces, classes, interfaces and enums, their public constants, properties and methods with their PHPDoc, and the options its factories take. Nothing of it is run.
ForgeBundleclassThe 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
ArtifactAttachCommandclassA 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
HoursLowBalanceCommandclassTells the clients whose support hours run low (under forge.low_balance_minutes, or overdrawn) - weekly, by cron.
1 method
LicenseExpireCommandclassMarks 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
LicenseKeypairCommandclassA 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
ReleaseSyncCommandclassNew 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
ActionControllerclassThe 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
BuildControllerclassA 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
PipelineControllerclassThe 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
RefundControllerclassRefunding 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
CardCrudControllerclassThe cards of the project boards (the clients move them from their dashboard).
3 methods
DownloadCrudControllerclassWho 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
HourCreditCrudControllerclassThe 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
HourPackCrudControllerclassHour packs: a market product (store, price, stock) plus the hours it credits.
3 methods
HourRefundCrudControllerclassThe refunds made, read-only: they are made from a purchase (RefundController).
4 methods
LicenseActivationCrudControllerclassThe 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
LicenseCrudControllerclassLicences: 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
LicenseOfferCrudControllerclassLicence offers: a market product that issues a licence of its software per unit paid.
3 methods
LicenseSeatCrudControllerclassThe 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
PipelineCrudControllerclassThe pipelines recorded: which project, which commit, where they ran - and each as its graph.
4 methods
ProjectCrudControllerclassClient projects: who, which repository, how many hours agreed.
4 methods
ProjectReviewCrudControllerclassThe 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
QuoteCrudControllerclassQuotes 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
ReleaseCrudControllerclassReleases: 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
SoftwareCrudControllerclassThe software (base-bundle threads: translated title, tagline and description; published, they are on the applications page).
3 methods
TimeEntryCrudControllerclassTime spent on client projects. Every billable entry comes off the client's support hours in the same save (TimeEntryLedgerSubscriber).
4 methods
AccountControllerclassThe 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
BoardControllerclassA 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
ComposerControllerclassThe Composer repository's single document. Anonymous: the free packages.
2 methods
DownloadControllerclassThe 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
HubControllerclassWhat 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
HubLinkControllerclassThe 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
LicenseApiControllerclassThe 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
PlayControllerclassA 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
ReviewControllerclassWhere 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
ShopControllerclassSupport, 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
UpmControllerclassThe Unity registry (UpmRegistry), as Unity's Package Manager reads a scoped registry: the search, the package documents, the tarballs.
5 methods
Base\Forge\DependencyInjection
ForgeConfigurationclass1 method
ForgeExtensionclass2 methods
ArtifactclassA 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
CardclassA task on a project's board. The client and the studio both move them.
17 methods
DownloadclassOne artifact handed out: who, under which licence, when. The IP is kept hashed.
7 methods
HourCreditclassOne 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
HourRefundclassHours 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
JobclassOne thing a stage runs: a test suite, a build, a deployment.
21 methods
LicenseclassThe 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
LicenseActivationclassOne 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
LicenseSeatclassOne 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
PipelineclassOne 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
ProjectclassA 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
ProjectReviewclassWhat 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
QuoteclassA 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
QuoteLineclassOne line of a quote: a piece of work, its hours, the rate.
16 methods
ReleaseclassOne 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
SoftwareclassSomething 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
StageclassA step of a pipeline ("test", "build", "deploy"): the jobs that run together, after the stage before.
12 methods
TimeEntryclassTime 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
HourPackclassSupport 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
LicenseOfferclassA licence for a piece of software, sold through base-bundle-market: paying for it issues a Forge\Entity\License per unit (OrderPaidSubscriber).
11 methods
CardColumnenumThe four columns of a project's board, in the order they are shown.
4 cases
CreditReasonenumWhy a client's support-hour balance moved.
4 cases
LicenseStatusenum3 cases
PipelineStatusenumWhere 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
PlatformenumA 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
PricingenumWhether a piece of software is downloaded freely or against a licence.
1 method · 2 cases
ProjectStatusenum3 cases
SoftwareKindenumWhat 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
SoftwareStatusenum3 cases
LicenseSeatListenerclassA 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
LicenseApiErrorSubscriberclassThe 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
OrderPaidSubscriberclassWhat a paid order delivers, since nothing here ships:
2 methods
TimeEntryLedgerSubscriberclassKeeps 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
HourRefundExceptionclassA refund HourRefunds would not make, or Stripe refused; nothing was changed.
LicenseActivationExceptionclassWhy an activation is refused: a reason the software can act on, and its HTTP status.
2 methods · 2 properties · 5 constants
QuoteLineTypeclassOne line of a quote in the back office: the work, its hours, the rate per hour (cents).
3 methods
ReviewAnswerTypeclassThe 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
ForgeRepositoryProviderclassHands 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
ActivationRequestclassWhat a piece of software sends to activate - or re-activate - its machine.
1 method · 6 properties
CardMoveclassA drop on a project's board: which column, at which position (BoardController, #[MapRequestPayload]).
3 methods · 2 properties
ReleaseRequestclassWhat a piece of software sends to free its own machine's place.
1 method · 2 properties
ReviewAnswerclassWhat the client answers about their project.
3 properties
HourCreditRepositoryclass5 methods
LicenseRepositoryclass7 methods
PipelineRepositoryclass4 methods
ProjectRepositoryclass4 methods
ProjectReviewRepositoryclass2 methods
QuoteRepositoryclassThe studio's quotes, numbered Q-<year>-<n>: what every quote table answers is omnibase/marketplace's (QuoteRepositoryTrait: saveNumbered(), findForClient(), pipeline()).
1 method
SoftwareRepositoryclass7 methods
ArtifactVoterclassWho 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
GitRepositoryVoterclassgit/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
ProjectVoterclassA 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
ArtifactBuilderclassBuilds 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
ArtifactFilesclassAn 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
BoardclassA 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
ComposerIndexclassThe 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
DeviceLinksclassAn 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
HourLedgerclassA 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
HourRefundsclassPaying 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
HubCatalogclassWhat 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
LicenseActivationsclassThe 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
LicenseHoldersclassWho 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
LicenseIssuerclassIssues licences (from a paid offer, or by hand) and the keys of their seats.
3 methods
LicenseKeyclassLicence 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
LicenseSeatsclassGives 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
LicenseTokenclassThe 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
PipelineGraphclassA 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
ProjectReviewsclassAsking 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
QuoteStatusGuardclassSmall 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
QuoteToOrderclassThe 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
ReleaseSyncclassTags 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
UpmRegistryclassThe 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
WebglBuildsclassA 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
ForgeExtensionclass{{ 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
PipelineExtensionclassWhat 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(): stringModern 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): voidBase\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): RedirectResponseActionController::send()
public function send(Request $request, int $id, MailerInterface $mailer): RedirectResponseActionController::review()
public function review(Request $request, int $id, ProjectReviews $reviews): RedirectResponseCloses the project - delivered - and asks its client for a review, by e-mail.
ActionController::paid()
public function paid(Request $request, string $reference, Checkout $checkout): RedirectResponseBuildController 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): ResponsePipelineController 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(): ResponsePipelineController::show()
public function show(int $id): ResponseRefundController 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): ResponseBase\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(): stringCardCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringCardCrudController::configureFields()
public function configureFields(string $pageName): iterableDownloadCrudController 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(): stringDownloadCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringDownloadCrudController::configureActions()
public function configureActions(Actions $actions): ActionsDownloadCrudController::configureFields()
public function configureFields(string $pageName): iterableHourCreditCrudController 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(): stringHourCreditCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringHourCreditCrudController::configureActions()
public function configureActions(Actions $actions): ActionsHourCreditCrudController::configureFields()
public function configureFields(string $pageName): iterableHourPackCrudController 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(): stringHourPackCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringHourPackCrudController::configureFields()
public function configureFields(string $pageName): iterableHourRefundCrudController 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(): stringHourRefundCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringHourRefundCrudController::configureActions()
public function configureActions(Actions $actions): ActionsHourRefundCrudController::configureFields()
public function configureFields(string $pageName): iterableLicenseActivationCrudController 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(): stringLicenseActivationCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringLicenseActivationCrudController::configureActions()
public function configureActions(Actions $actions): ActionsLicenseActivationCrudController::configureFields()
public function configureFields(string $pageName): iterableLicenseCrudController 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(): stringLicenseCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringLicenseCrudController::configureFields()
public function configureFields(string $pageName): iterableLicenseOfferCrudController 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(): stringLicenseOfferCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringLicenseOfferCrudController::configureFields()
public function configureFields(string $pageName): iterableLicenseSeatCrudController 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(): stringLicenseSeatCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringLicenseSeatCrudController::configureFields()
public function configureFields(string $pageName): iterablePipelineCrudController 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(): stringPipelineCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringPipelineCrudController::configureActions()
public function configureActions(Actions $actions): ActionsPipelineCrudController::configureFields()
public function configureFields(string $pageName): iterableProjectCrudController class
class ProjectCrudController extends AbstractCrudController ProjectCrudController.php:18
Client projects: who, which repository, how many hours agreed.
ProjectCrudController::getEntityFqcn()
public static function getEntityFqcn(): stringProjectCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringProjectCrudController::configureActions()
public function configureActions(Actions $actions): ActionsProjectCrudController::configureFields()
public function configureFields(string $pageName): iterableProjectReviewCrudController 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(): stringProjectReviewCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringProjectReviewCrudController::configureActions()
public function configureActions(Actions $actions): ActionsProjectReviewCrudController::configureFields()
public function configureFields(string $pageName): iterableQuoteCrudController 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(): stringQuoteCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringQuoteCrudController::configureActions()
public function configureActions(Actions $actions): ActionsQuoteCrudController::configureFields()
public function configureFields(string $pageName): iterableQuoteCrudController::createEntity()
public function createEntity(string $entityFqcn): objectQuoteCrudController::persistEntity()
public function persistEntity(EntityManagerInterface $entityManager, object $entity): voidNumbered 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(): stringReleaseCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringReleaseCrudController::configureActions()
public function configureActions(Actions $actions): ActionsReleaseCrudController::configureFields()
public function configureFields(string $pageName): iterableSoftwareCrudController 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(): stringSoftwareCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringSoftwareCrudController::configureFields()
public function configureFields(string $pageName): iterableTimeEntryCrudController 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(): stringTimeEntryCrudController::getPreferredIcon()
public static function getPreferredIcon(): ?stringTimeEntryCrudController::configureFields()
public function configureFields(string $pageName): iterableTimeEntryCrudController::createEntity()
public function createEntity(string $entityFqcn): objectBase\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(): ResponseAccountController::rekey()
public function rekey(Request $request, int $id, LicenseIssuer $issuer): ResponseA 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): ResponseThe 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): ResponseThe owner takes a seat back: its key stops working at once.
AccountController::releaseMachine()
public function releaseMachine(Request $request, int $id, int $activation, LicenseActivations $activations): ResponseFrees 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): ResponseBoardController::move()
public function move(int $id, Request $request, CardMove $move): JsonResponseComposerController 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): ResponseDownloadController 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(): ResponseDownloadController::software()
public function software(string $slug): ResponseDownloadController::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): ResponseHubController 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(): JsonResponseHubController::catalog()
public function catalog(Request $request): JsonResponseHubController::licenses()
public function licenses(Request $request): JsonResponseThe key's licences and the machines of its seat (a hub client's Licences page).
HubController::device()
public function device(Request $request): JsonResponseThe editor asks for a code to show its user; the link page takes it.
HubController::token()
public function token(Request $request): JsonResponseThe 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)HubLinkController::link()
public function link(Request $request): ResponseLicenseApiController 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): JsonResponseActivates 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): JsonResponseFrees the machine's place: 200 whether it held one or not; 401 a wrong key.
LicenseApiController::publicKey()
public function publicKey(LicenseToken $tokens): JsonResponseThe 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): ResponseReviewController 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): ResponseShopController 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(): ResponseShopController::request()
public function request(Request $request, MailerInterface $mailer, CompanyRegistry $registry, ?string $contact = null): ResponseShopController::quote()
public function quote(string $token): ResponseShopController::accept()
public function accept(Request $request, string $token, QuoteToOrder $quoteToOrder): ResponseUpmController 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)UpmController::search()
public function search(Request $request): ResponseUpmController::all()
public function all(Request $request): ResponseUpmController::package()
public function package(Request $request, string $name): ResponseUpmController::tarball()
public function tarball(Request $request, string $name, string $filename): ResponseBase\Forge\DependencyInjection
ForgeConfiguration class
class ForgeConfiguration implements ConfigurationInterface ForgeConfiguration.php:8
ForgeConfiguration::getConfigTreeBuilder()
public function getConfigTreeBuilder(): TreeBuilderForgeExtension class
class ForgeExtension extends AbstractBaseExtension ForgeExtension.php:11
ForgeExtension::getConfiguration()
public function getConfiguration(array $config, ContainerBuilder $container): ForgeConfigurationForgeExtension::load()
public function load(array $configs, ContainerBuilder $container): voidBase\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(): stringArtifact::getId()
public function getId(): ?intArtifact::getRelease()
public function getRelease(): ?ReleaseArtifact::setRelease()
public function setRelease(?Release $release): selfArtifact::getSoftware()
public function getSoftware(): ?SoftwareArtifact::getPath()
public function getPath(): stringArtifact::getFilename()
public function getFilename(): stringArtifact::getFormat()
public function getFormat(): stringArtifact::getSize()
public function getSize(): intArtifact::getSha256()
public function getSha256(): stringArtifact::getSha1()
public function getSha1(): ?stringArtifact::getSha512()
public function getSha512(): ?stringArtifact::getIntegrity()
public function getIntegrity(): ?stringSubresource integrity, as npm writes it: sha512-<base64>.
Artifact::getPlatform()
public function getPlatform(): ?PlatformThe platform it is the build for, or null: a source archive, a package.
Artifact::getContentType()
public function getContentType(): stringWhat it is served as.
Artifact::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableArtifact::isFree()
public function isFree(): boolCard 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(): stringCard::getId()
public function getId(): ?intCard::getProject()
public function getProject(): ?ProjectCard::setProject()
public function setProject(?Project $project): selfCard::getTitle()
public function getTitle(): stringCard::setTitle()
public function setTitle(string $title): selfCard::getDescription()
public function getDescription(): ?stringCard::setDescription()
public function setDescription(?string $description): selfCard::getColumn()
public function getColumn(): CardColumnCard::getPosition()
public function getPosition(): intCard::moveTo()
public function moveTo(CardColumn $column, int $position): selfCard::setColumn()
public function setColumn(CardColumn $column): selfCard::setPosition()
public function setPosition(int $position): selfCard::getLabel()
public function getLabel(): ?stringCard::setLabel()
public function setLabel(?string $label): selfCard::getUpdatedAt()
public function getUpdatedAt(): \DateTimeImmutableDownload 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(): ?intDownload::getArtifact()
public function getArtifact(): ArtifactDownload::getUser()
public function getUser(): ?UserDownload::getLicense()
public function getLicense(): ?LicenseDownload::getChannel()
public function getChannel(): stringDownload::getDownloadedAt()
public function getDownloadedAt(): \DateTimeImmutableHourCredit 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(): ?intHourCredit::getUser()
public function getUser(): UserHourCredit::setUser()
public function setUser(User $user): selfA time entry's debit follows it to another client's project (TimeEntryLedgerSubscriber).
HourCredit::getMinutes()
public function getMinutes(): intHourCredit::setMinutes()
public function setMinutes(int $minutes): selfHourCredit::getReason()
public function getReason(): CreditReasonHourCredit::getNote()
public function getNote(): ?stringHourCredit::getOrderReference()
public function getOrderReference(): ?stringHourCredit::setOrderReference()
public function setOrderReference(?string $orderReference): selfHourCredit::getTimeEntry()
public function getTimeEntry(): ?TimeEntryHourCredit::setTimeEntry()
public function setTimeEntry(?TimeEntry $timeEntry): selfHourCredit::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableHourRefund 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(): stringHourRefund::getId()
public function getId(): ?intHourRefund::getCustomer()
public function getCustomer(): ?UserHourRefund::getOrderReference()
public function getOrderReference(): stringHourRefund::getAmount()
public function getAmount(): intHourRefund::getCurrency()
public function getCurrency(): stringHourRefund::getMinutes()
public function getMinutes(): intHourRefund::getReason()
public function getReason(): stringHourRefund::getMethod()
public function getMethod(): stringHourRefund::isByTransfer()
public function isByTransfer(): boolHourRefund::getStripeRefund()
public function getStripeRefund(): ?stringHourRefund::setStripeRefund()
public function setStripeRefund(?string $stripeRefund): selfHourRefund::getRefundedBy()
public function getRefundedBy(): ?UserHourRefund::setRefundedBy()
public function setRefundedBy(?User $refundedBy): selfHourRefund::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableJob 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(): stringJob::getId()
public function getId(): ?intJob::getStage()
public function getStage(): ?StageJob::setStage()
public function setStage(?Stage $stage): selfJob::getName()
public function getName(): stringJob::setName()
public function setName(string $name): selfJob::getStatus()
public function getStatus(): PipelineStatusJob::setStatus()
public function setStatus(PipelineStatus $status): selfJob::getEffectiveStatus()
public function getEffectiveStatus(): PipelineStatusIts status as its stage counts it: a failure that was allowed is a warning.
Job::isAllowFailure()
public function isAllowFailure(): boolJob::setAllowFailure()
public function setAllowFailure(bool $allowFailure): selfJob::getNeeds()
public function getNeeds(): arrayJob::setNeeds()
public function setNeeds(array $needs): selfJob::getUrl()
public function getUrl(): ?stringJob::setUrl()
public function setUrl(?string $url): selfJob::getStartedAt()
public function getStartedAt(): ?\DateTimeImmutableJob::setStartedAt()
public function setStartedAt(?\DateTimeImmutable $startedAt): selfJob::getFinishedAt()
public function getFinishedAt(): ?\DateTimeImmutableJob::setFinishedAt()
public function setFinishedAt(?\DateTimeImmutable $finishedAt): selfJob::getDuration()
public function getDuration(): ?intSeconds 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(): stringLicense::getId()
public function getId(): ?intLicense::getSoftware()
public function getSoftware(): ?SoftwareLicense::setSoftware()
public function setSoftware(Software $software): selfLicense::getOwner()
public function getOwner(): ?UserLicense::setOwner()
public function setOwner(User $owner): selfLicense::getHolders()
public function getHolders(): Collection- Returns
the e-mails it is given to, first given first
License::getMachinesPerSeat()
public function getMachinesPerSeat(): intLicense::setMachinesPerSeat()
public function setMachinesPerSeat(int $machines): selfLicense::getSeatsTaken()
public function getSeatsTaken(): intLicense::getSeatsLeft()
public function getSeatsLeft(): intLicense::findSeat()
public function findSeat(string $email): ?LicenseSeatThe seat given to that e-mail, if any.
License::findSeatOf()
public function findSeatOf(User $user): ?LicenseSeatThe seat that user holds, if any.
License::addHolder()
public function addHolder(LicenseSeat $seat): selfGives a seat to that e-mail - not checked against the seats left: LicenseSeats does.
License::removeHolder()
public function removeHolder(LicenseSeat $seat): selfLicense::getStatus()
public function getStatus(): LicenseStatusLicense::setStatus()
public function setStatus(LicenseStatus $status): selfLicense::getSeats()
public function getSeats(): intLicense::setSeats()
public function setSeats(int $seats): selfNever fewer than the seats already given: take some back first.
License::getExpiresAt()
public function getExpiresAt(): ?\DateTimeImmutableLicense::setExpiresAt()
public function setExpiresAt(?\DateTimeImmutable $expiresAt): selfLicense::getUpdatesUntil()
public function getUpdatesUntil(): ?\DateTimeImmutableLicense::setUpdatesUntil()
public function setUpdatesUntil(?\DateTimeImmutable $updatesUntil): selfLicense::getOrderReference()
public function getOrderReference(): ?stringLicense::setOrderReference()
public function setOrderReference(?string $orderReference): selfLicense::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutableLicense::isValid()
public function isValid(?\DateTimeImmutable $at = null): boolLicense::covers()
public function covers(Release $release, ?\DateTimeImmutable $at = null): boolWhether 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): stringThe hash a machine is known by: the one its software can compute too.
LicenseActivation::__toString()
public function __toString(): stringLicenseActivation::getId()
public function getId(): ?intLicenseActivation::getSeat()
public function getSeat(): ?LicenseSeatLicenseActivation::setSeat()
public function setSeat(?LicenseSeat $seat): selfLicenseActivation::getMachine()
public function getMachine(): stringLicenseActivation::getName()
public function getName(): ?stringLicenseActivation::setName()
public function setName(?string $name): selfLicenseActivation::getPlatform()
public function getPlatform(): ?stringLicenseActivation::setPlatform()
public function setPlatform(?string $platform): selfLicenseActivation::getVersion()
public function getVersion(): ?stringLicenseActivation::setVersion()
public function setVersion(?string $version): selfLicenseActivation::getActivatedAt()
public function getActivatedAt(): \DateTimeImmutableLicenseActivation::getLastSeenAt()
public function getLastSeenAt(): \DateTimeImmutableLicenseActivation::seen()
public function seen(?\DateTimeImmutable $at = null): selfLicenseSeat 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(): stringLicenseSeat::getId()
public function getId(): ?intLicenseSeat::getLicense()
public function getLicense(): ?LicenseLicenseSeat::setLicense()
public function setLicense(?License $license): selfLicenseSeat::getEmail()
public function getEmail(): stringLicenseSeat::setEmail()
public function setEmail(string $email): selfLicenseSeat::getUser()
public function getUser(): ?UserLicenseSeat::setUser()
public function setUser(?User $user): selfLicenseSeat::getAssignedAt()
public function getAssignedAt(): \DateTimeImmutableLicenseSeat::getActivations()
public function getActivations(): CollectionLicenseSeat::findActivation()
public function findActivation(string $machine): ?LicenseActivationLicenseSeat::addActivation()
public function addActivation(LicenseActivation $activation): selfLicenseSeat::removeActivation()
public function removeActivation(LicenseActivation $activation): selfLicenseSeat::getMachinesLeft()
public function getMachinesLeft(): intMachines it may still be activated on.
LicenseSeat::isNotified()
public function isNotified(): boolLicenseSeat::markNotified()
public function markNotified(): selfLicenseSeat::isHeldBy()
public function isHeldBy(User $user): boolWhether 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(): ?stringLicenseSeat::hasKey()
public function hasKey(): boolLicenseSeat::setKey()
public function setKey(string $key): selfStores the key's hash; the key itself is the caller's to show once.
LicenseSeat::validateSeats()
public function validateSeats(ExecutionContextInterface $context): voidNot more seats given than the licence has.
LicenseSeat::matchesKey()
public function matchesKey(string $key): boolPipeline 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(): stringPipeline::getId()
public function getId(): ?intPipeline::getProject()
public function getProject(): ?ProjectPipeline::setProject()
public function setProject(?Project $project): selfPipeline::getRef()
public function getRef(): ?stringPipeline::setRef()
public function setRef(?string $ref): selfPipeline::getCommitSha()
public function getCommitSha(): ?stringPipeline::setCommitSha()
public function setCommitSha(?string $commitSha): selfPipeline::getShortSha()
public function getShortSha(): ?stringPipeline::getSource()
public function getSource(): ?stringPipeline::getExternalId()
public function getExternalId(): ?stringPipeline::setSource()
public function setSource(?string $source, ?string $externalId = null): selfPipeline::getUrl()
public function getUrl(): ?stringPipeline::setUrl()
public function setUrl(?string $url): selfPipeline::getStatus()
public function getStatus(): PipelineStatusWhat was set by hand, or what the stages say.
Pipeline::setStatus()
public function setStatus(?PipelineStatus $status): selfPipeline::getCreatedAt()
public function getCreatedAt(): \DateTimeImmutablePipeline::setCreatedAt()
public function setCreatedAt(\DateTimeImmutable $createdAt): selfPipeline::getFinishedAt()
public function getFinishedAt(): ?\DateTimeImmutablePipeline::setFinishedAt()
public function setFinishedAt(?\DateTimeImmutable $finishedAt): selfPipeline::getDuration()
public function getDuration(): ?intSeconds 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): StageThe 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(): ?arrayProject::__construct()
public function __construct(?User $client = null, ?string $title = null, ?string $slug = null)Project::getClient()
public function getClient(): ?UserProject::setClient()
public function setClient(User $client): selfProject::getName()
public function getName(): stringThe title, as the dashboard calls it.
Project::setName()
public function setName(string $name): selfProject::getSummary()
public function getSummary(): ?stringOne line under the name: the thread's (translated) excerpt.
Project::setSummary()
public function setSummary(?string $summary): selfProject::getRepositoryUrl()
public function getRepositoryUrl(): ?stringProject::setRepositoryUrl()
public function setRepositoryUrl(?string $repositoryUrl): selfProject::getGitRepository()
public function getGitRepository(): ?stringProject::setGitRepository()
public function setGitRepository(?string $gitRepository): selfProject::getStatus()
public function getStatus(): ProjectStatusProject::setStatus()
public function setStatus(ProjectStatus $status): selfProject::getBudgetMinutes()
public function getBudgetMinutes(): ?intProject::setBudgetMinutes()
public function setBudgetMinutes(?int $budgetMinutes): selfProject::getDueOn()
public function getDueOn(): ?\DateTimeImmutableProject::setDueOn()
public function setDueOn(?\DateTimeImmutable $dueOn): selfProject::getCards()
public function getCards(): CollectionProject::getBoard()
public function getBoard(): array- Returns
the board, column by column, in position order
Project::addCard()
public function addCard(Card $card): selfProject::getTimeEntries()
public function getTimeEntries(): Collection- Returns
newest first
Project::addTimeEntry()
public function addTimeEntry(TimeEntry $entry): selfProject::getSpentMinutes()
public function getSpentMinutes(): intProject::getProgress()
public function getProgress(): int0-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(): stringProjectReview::getId()
public function getId(): ?intProjectReview::getProject()
public function getProject(): ?ProjectProjectReview::getToken()
public function getToken(): stringProjectReview::getRequestedAt()
public function getRequestedAt(): \DateTimeImmutableProjectReview::renew()
public function renew(): selfAsked again (the first e-mail lost): a new link, the old one void.
ProjectReview::getRating()
public function getRating(): ?intProjectReview::getComment()
public function getComment(): ?stringProjectReview::setComment()
public function setComment(?string $comment): selfProjectReview::getSignature()
public function getSignature(): ?stringProjectReview::setSignature()
public function setSignature(?string $signature): selfProjectReview::getSubmittedAt()
public function getSubmittedAt(): ?\DateTimeImmutableProjectReview::isSubmitted()
public function isSubmitted(): boolProjectReview::submit()
public function submit(int $rating, ?string $comment, ?string $signature): selfThe client's answer: once.
ProjectReview::isPublished()
public function isPublished(): boolProjectReview::setPublished()
public function setPublished(bool $published): selfQuote 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(): boolHours to sell: a quote without any cannot be accepted.
Quote::getLines()
public function getLines(): CollectionQuote::addLine()
public function addLine(QuoteLine $line): selfQuote::removeLine()
public function removeLine(QuoteLine $line): selfQuote::getTotalMinutes()
public function getTotalMinutes(): intQuote::getSubtotal()
public function getSubtotal(): intBefore the discount, in cents.
Quote::getProduct()
public function getProduct(): ?HourPackQuote::setProduct()
public function setProduct(?HourPack $product): selfQuote::getProject()
public function getProject(): ?ProjectQuote::setProject()
public function setProject(?Project $project): selfQuoteLine 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(): stringQuoteLine::getId()
public function getId(): ?intQuoteLine::getQuote()
public function getQuote(): ?QuoteQuoteLine::setQuote()
public function setQuote(?Quote $quote): selfQuoteLine::getLabel()
public function getLabel(): stringQuoteLine::setLabel()
public function setLabel(string $label): selfQuoteLine::getMinutes()
public function getMinutes(): intQuoteLine::setMinutes()
public function setMinutes(int $minutes): selfQuoteLine::getHours()
public function getHours(): floatHours as the admin form edits them (1.5 = 1h30).
QuoteLine::setHours()
public function setHours(float $hours): selfQuoteLine::getHourlyRate()
public function getHourlyRate(): intQuoteLine::setHourlyRate()
public function setHourlyRate(int $hourlyRate): selfQuoteLine::getPosition()
public function getPosition(): intQuoteLine::setPosition()
public function setPosition(int $position): selfQuoteLine::getAmount()
public function getAmount(): intIn 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(): stringRelease::getId()
public function getId(): ?intRelease::getSoftware()
public function getSoftware(): ?SoftwareRelease::setSoftware()
public function setSoftware(?Software $software): selfRelease::getVersion()
public function getVersion(): stringRelease::setVersion()
public function setVersion(string $version): selfRelease::getTag()
public function getTag(): ?stringRelease::setTag()
public function setTag(?string $tag): selfRelease::getCommitSha()
public function getCommitSha(): ?stringRelease::setCommitSha()
public function setCommitSha(?string $commitSha): selfRelease::getChangelog()
public function getChangelog(): ?stringRelease::setChangelog()
public function setChangelog(?string $changelog): selfRelease::getPublishedAt()
public function getPublishedAt(): ?\DateTimeImmutableRelease::setPublishedAt()
public function setPublishedAt(?\DateTimeImmutable $publishedAt): selfRelease::isPublished()
public function isPublished(): boolRelease::getArtifacts()
public function getArtifacts(): CollectionRelease::addArtifact()
public function addArtifact(Artifact $artifact): selfRelease::getPlatformArtifacts()
public function getPlatformArtifacts(): array- Returns
its builds, one per platform, in Platform's order
Release::getArtifact()
public function getArtifact(string $format = 'zip'): ?ArtifactSoftware 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(): ?arraySoftware::__toLink()
public function __toLink(array $routeParameters = [], int $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH): ?stringSoftware::__construct()
public function __construct(?User $owner = null, ?string $title = null, ?string $slug = null)Software::setParent()
public function setParent(?Thread $parent): selfIts 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(): boolListed on its own: no parent, or one that is not published.
Software::getName()
public function getName(): stringThe title, as the templates and the admin call it.
Software::setName()
public function setName(string $name): selfSoftware::getTagline()
public function getTagline(): ?stringThe one-liner under the name: the thread's (translated) headline.
Software::setTagline()
public function setTagline(?string $tagline): selfSoftware::getDescription()
public function getDescription(): ?stringThe long text: the thread's (translated) content.
Software::setDescription()
public function setDescription(?string $description): selfSoftware::publish()
public function publish(bool $visible = true): selfPublish (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(): stringSoftware::setCategory()
public function setCategory(string $category): selfSoftware::getStatus()
public function getStatus(): SoftwareStatusSoftware::setStatus()
public function setStatus(SoftwareStatus $status): selfSoftware::getKind()
public function getKind(): SoftwareKindSoftware::setKind()
public function setKind(SoftwareKind $kind): selfSoftware::isGame()
public function isGame(): boolSoftware::hasPlayablePreview()
public function hasPlayablePreview(): boolSoftware::getPlayablePreview()
public function getPlayablePreview(): boolSoftware::setPlayablePreview()
public function setPlayablePreview(?bool $preview): selfSoftware::isPlayableInBrowser()
public function isPlayableInBrowser(): boolWhether 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(): PricingSoftware::setPricing()
public function setPricing(Pricing $pricing): selfSoftware::isFree()
public function isFree(): boolSoftware::getStack()
public function getStack(): arraySoftware::setStack()
public function setStack(array $stack): selfSoftware::getStackAsText()
public function getStackAsText(): stringThe stack as the back office edits it: "Symfony, TransparentJS".
Software::setStackAsText()
public function setStackAsText(?string $stack): selfSoftware::getHomepage()
public function getHomepage(): ?stringSoftware::setHomepage()
public function setHomepage(?string $homepage): selfSoftware::getSourceUrl()
public function getSourceUrl(): ?stringSoftware::setSourceUrl()
public function setSourceUrl(?string $sourceUrl): selfSoftware::getRepositoryUrl()
public function getRepositoryUrl(): ?stringSoftware::setRepositoryUrl()
public function setRepositoryUrl(?string $repositoryUrl): selfSoftware::getGitRepository()
public function getGitRepository(): ?stringSoftware::setGitRepository()
public function setGitRepository(?string $gitRepository): selfSoftware::getPackageName()
public function getPackageName(): ?stringSoftware::setPackageName()
public function setPackageName(?string $packageName): selfSoftware::getUpmName()
public function getUpmName(): ?stringSoftware::setUpmName()
public function setUpmName(?string $upmName): selfSoftware::getImage()
public function getImage(): ?stringThe 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): selfSoftware::getScreenshots()
public function getScreenshots(): array- Returns
the screenshots' public paths, in their order
Software::getScreenshotFiles()
public function getScreenshotFiles()Software::setScreenshots()
public function setScreenshots($screenshots): selfSoftware::getTrailer()
public function getTrailer(): ?stringThe trailer's public path (an upload), or the stored URL as it is.
Software::getTrailerFile()
public function getTrailerFile()Software::setTrailer()
public function setTrailer($trailer): selfSoftware::getDemoUrl()
public function getDemoUrl(): ?stringSoftware::setDemoUrl()
public function setDemoUrl(?string $demoUrl): selfSoftware::getPosition()
public function getPosition(): intSoftware::setPosition()
public function setPosition(int $position): selfSoftware::getYear()
public function getYear(): ?stringSoftware::setYear()
public function setYear(?string $year): selfSoftware::getReleases()
public function getReleases(): Collection- Returns
newest first
Software::addRelease()
public function addRelease(Release $release): selfSoftware::removeRelease()
public function removeRelease(Release $release): selfA release never saved, taken back (its build failed): the next flush must not cascade it in.
Software::getLatestRelease()
public function getLatestRelease(): ?ReleaseSoftware::findRelease()
public function findRelease(string $version): ?ReleaseSoftware::getOffers()
public function getOffers(): CollectionStage 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(): stringStage::getId()
public function getId(): ?intStage::getPipeline()
public function getPipeline(): ?PipelineStage::setPipeline()
public function setPipeline(?Pipeline $pipeline): selfStage::getName()
public function getName(): stringStage::setName()
public function setName(string $name): selfStage::getPosition()
public function getPosition(): intStage::setPosition()
public function setPosition(int $position): selfStage::getJobs()
public function getJobs(): CollectionStage::job()
public function job(string $name, PipelineStatus $status = PipelineStatus::PENDING): JobThe job of that name, made when there is none.
Stage::getStatus()
public function getStatus(): PipelineStatusWhat 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(): stringTimeEntry::formatMinutes()
public static function formatMinutes(int $minutes): stringTimeEntry::getId()
public function getId(): ?intTimeEntry::getProject()
public function getProject(): ?ProjectTimeEntry::setProject()
public function setProject(?Project $project): selfTimeEntry::getAuthor()
public function getAuthor(): ?UserTimeEntry::setAuthor()
public function setAuthor(?User $author): selfTimeEntry::getMinutes()
public function getMinutes(): intTimeEntry::setMinutes()
public function setMinutes(int $minutes): selfTimeEntry::getNote()
public function getNote(): stringTimeEntry::setNote()
public function setNote(string $note): selfTimeEntry::getCommitSha()
public function getCommitSha(): ?stringTimeEntry::setCommitSha()
public function setCommitSha(?string $commitSha): selfTimeEntry::getSpentOn()
public function getSpentOn(): \DateTimeImmutableTimeEntry::setSpentOn()
public function setSpentOn(\DateTimeImmutable $spentOn): selfTimeEntry::isBillable()
public function isBillable(): boolTimeEntry::setBillable()
public function setBillable(bool $billable): selfBase\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(): boolTime, not goods: checkout asks for no address.
HourPack::getMaxQuantity()
public function getMaxQuantity(): ?intA quote's own pack is bought once; listed packs, as many as wanted.
HourPack::getMinutes()
public function getMinutes(): intHourPack::setMinutes()
public function setMinutes(int $minutes): selfHourPack::getHours()
public function getHours(): floatHourPack::setHours()
public function setHours(float $hours): selfHourPack::isListed()
public function isListed(): boolHourPack::setListed()
public function setListed(bool $listed): selfHourPack::getHourlyPrice()
public function getHourlyPrice(): intIn 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(): boolA key and downloads, nothing posted: checkout asks for no address.
LicenseOffer::getSoftware()
public function getSoftware(): ?SoftwareLicenseOffer::setSoftware()
public function setSoftware(?Software $software): selfLicenseOffer::getDurationMonths()
public function getDurationMonths(): ?intLicenseOffer::setDurationMonths()
public function setDurationMonths(?int $durationMonths): selfLicenseOffer::getUpdatesMonths()
public function getUpdatesMonths(): ?intLicenseOffer::setUpdatesMonths()
public function setUpdatesMonths(?int $updatesMonths): selfLicenseOffer::getMachinesPerSeat()
public function getMachinesPerSeat(): intLicenseOffer::setMachinesPerSeat()
public function setMachinesPerSeat(?int $machines): selfLicenseOffer::getSeats()
public function getSeats(): intLicenseOffer::setSeats()
public function setSeats(int $seats): selfBase\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(): boolNothing more will happen by itself.
PipelineStatus::of()
public static function of(iterable $parts): selfThe 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(): stringPlatform::icon()
public function icon(): stringPlatform::extensions()
public function extensions(): array- Returns
the file extensions its build may come as
Platform::values()
public static function values(): arrayPricing 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(): boolProjectStatus 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): voidBase\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(): arrayLicenseApiErrorSubscriber::onException()
public function onException(ExceptionEvent $event): voidOrderPaidSubscriber 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): voidTimeEntryLedgerSubscriber 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): voidBase\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 $reasonpublic readonly array $details
LicenseActivationException::__construct()
public function __construct(public readonly string $reason, public readonly array $details = [])LicenseActivationException::getStatus()
public function getStatus(): intBase\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): voidThe 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): voidReviewAnswerType 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): voidReviewAnswerType::configureOptions()
public function configureOptions(OptionsResolver $resolver): voidBase\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(): arrayBase\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 $keyThe seat's licence key: glk_…
public readonly string $softwareThe software's slug on the forge: a key only opens its own software.
public readonly string $machineA stable identifier of the machine (Unity: SystemInfo.deviceUniqueIdentifier; macOS: IOPlatformUUID). Hashed, never stored as is.
public readonly ?string $nameWhat the machine is called, to recognise it by: "MacBook Air de Camille".
public readonly ?string $platformmacos, windows, linux, android, ios.
..
public readonly ?string $versionThe 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 $columnpublic readonly int $position
CardMove::__construct()
public function __construct(public readonly string $column = '', public readonly int $position = 0)CardMove::columns()
public static function columns(): arrayCardMove::getColumn()
public function getColumn(): CardColumnReleaseRequest class
final class ReleaseRequest ReleaseRequest.php:8
What a piece of software sends to free its own machine's place.
public readonly string $keypublic 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 = nullpublic ?string $comment = nullpublic ?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): intMinutes 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): boolLicenseRepository 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): ?LicenseSeatThe 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): ?PipelineThe 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): ?ProjectProjectReviewRepository 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): ArtifactArtifactBuilder::buildUpm()
public function buildUpm(Release $release): ArtifactThe 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): ArtifactA 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): boolA zip with an index.html and a Build/ folder at its root, or under one folder.
ArtifactBuilder::webglRoot()
public static function webglRoot(\ZipArchive $zip): ?stringThe 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): stringArtifactBuilder::tar()
public static function tar(array $files, int $mtime = 0): stringA 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$channelweb, 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): voidPut 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): CardA 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$licensesthe caller's valid licences (empty: anonymous)
ComposerIndex::authenticate()
public function authenticate(?string $email, ?string $key): arrayWho 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
DeviceLinks class
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.
const TTL = 600const INTERVAL = 3
DeviceLinks::__construct()
public function __construct(readonly CacheItemPoolInterface $cache, readonly LicenseRepository $licenses, readonly LicenseIssuer $issuer)DeviceLinks::start()
public function start(?string $item = null): arrayDeviceLinks::find()
public function find(string $userCode): ?array- Returns
the code a user typed, while it waits
DeviceLinks::approve()
public function approve(string $userCode, User $user): ?LicenseSeatThe 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.
DeviceLinks::poll()
public function poll(string $deviceCode): arrayWhat 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).
DeviceLinks::seatOf()
public function seatOf(User $user): ?LicenseSeatA 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): intHourLedger::history()
public function history(User $user, int $limit = 50): arrayHourLedger::credit()
public function credit(User $user, int $minutes, CreditReason $reason = CreditReason::PURCHASE, ?string $note = null, ?string $orderReference = null): HourCreditPersisted, not flushed.
HourLedger::debitFor()
public function debitFor(TimeEntry $entry, ?HourCredit $existing): ?HourCreditThe 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): ?OrderHourRefunds::summary()
public function summary(Order $order): arrayWhat this order can still give back.
HourRefunds::refunds()
public function refunds(string $reference): arrayHourRefunds::refund()
public function refund(Order $order, int $amount, int $minutes, string $reason, ?User $by = null): HourRefundRefunds $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(): arrayThe hub manifest: version 1. A source without "checkout" offers nothing to buy.
HubCatalog::catalog()
public function catalog(array $licenses = []): arrayThe 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.
$licensesthe caller's valid licences (the Bearer key of the request)
HubCatalog::licenses()
public function licenses(array $licenses, string $email): arrayThe 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): arrayActivates the machine - or finds it already activated - and signs it a token. Flushed.
- Throws
LicenseActivationException
LicenseActivations::releaseMachine()
public function releaseMachine(string $key, string $machineId): boolThe 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): voidFrees 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$emailwhen 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): ?arrayThe 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): LicenseA 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): stringA 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(): stringLicenseKey::looksValid()
public static function looksValid(string $key): boolLicenseSeats 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): LicenseSeatA 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): voidTakes the seat back: its key stops working at once. Flushed.
LicenseSeats::welcome()
public function welcome(LicenseSeat $seat): voidA 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(): boolLicenseToken::publicKey()
public function publicKey(): ?stringThe public key the software ships with, base64.
LicenseToken::issue()
public function issue(LicenseActivation $activation, ?\DateTimeImmutable $at = null): arrayLicenseToken::verify()
public static function verify(string $token, string $publicKey): ?arrayWhat 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): arrayPipelineGraph::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): stringA 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): ProjectReviewCloses 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): boolAccepted, 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): OrderReleaseSync 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): voidRebuild 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): ?arrayWho 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): ?SoftwareUpmRegistry::document()
public function document(Software $software, array $licenses = []): arrayThe 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).
$licensesthe caller's valid licences
UpmRegistry::search()
public function search(string $text, int $from = 0, int $size = 20, array $licenses = []): arrayThe 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 = []): arrayThe 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 = []): ?arrayThe tarball of that file name, when the caller may have it.
UpmRegistry::release()
public function release(Software $software, string $filename): ?ReleaseThe published release that tarball is of, whoever may have it (a 401/403 rather than a 404).
UpmRegistry::hasVersions()
public function hasVersions(Software $software): boolWhether 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): ?arrayThe 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): stringWhere the build is unpacked: made the first time it is asked for.
WebglBuilds::headers()
public static function headers(string $path): arrayBase\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): stringForgeExtension::money()
public function money(?int $cents, string $currency = 'EUR', ?string $locale = null): stringForgeExtension::price()
public function price(?int $cents, string $currency = 'EUR', ?string $locale = null): stringA 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