Download the PHP package nubitio/admin-bundle without Composer

On this page you can find all versions of the php package nubitio/admin-bundle. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package admin-bundle

nubitio/admin-bundle

One-line backend for the Nubit admin stack. Install it, point @nubitio/react-admin at your API, and you have a CRUD admin system.

Registers automatically:

Setup

  1. Import the routes (config/routes/nubit_admin.yaml):

  2. Wire the firewall (config/packages/security.yaml) — the bundle cannot define firewalls for you. Apps with more than one user provider (e.g. an extra admin firewall) must also alias the one the API uses, otherwise autowiring is ambiguous:

  3. Create the refresh-token table: bin/console make:migration && bin/console doctrine:migrations:migrate (the bundle's RefreshToken entity is auto-mapped).

CSRF policy for cookie-authenticated mutations

The API accepts the JWT either as a Authorization: Bearer header or as the HttpOnly AUTH_TOKEN/REFRESH_TOKEN cookie. A cookie is attached by the browser automatically, even on a cross-site request — SameSite=Strict on both cookies (CookieFactory) already blocks the simple case, but it is not a complete contract by itself: some reverse-proxy setups strip or rewrite Set-Cookie, and a cross-subdomain deployment (domain: .example.com) opts every subdomain back into being "same-site" for each other.

CsrfProtectionListener closes that gap with a stateless double-submit token — no server-side session is needed, which matters because the api firewall is stateless: true:

Exempt by design: requests authenticated with Authorization: Bearer (mobile/API clients) or X-Api-Key (ApiKeyAuthenticator, identity module) never carry the cookie automatically, so they are not vulnerable to CSRF the same way and need no token — this keeps them usable with zero browser coupling. POST /api/auth/login is exempt too: it is what issues the CSRF_TOKEN cookie in the first place, so it cannot require a token that does not exist yet (a stale cookie from a previous session must not block a fresh login, the same call JWTAuthenticator already makes for the JWT cookie itself).

Reverse proxies and cross-subdomain deployments: the policy needs no special handling there — it never inspects Origin/Referer or depends on SameSite being honored, only on the CSRF_TOKEN cookie and the header reaching the app together. The one requirement is that Set-Cookie and custom request headers both pass through unmodified end to end; a proxy that strips either breaks the policy (and, for the CSRF cookie, breaks it closed — the request is rejected, not silently allowed). Set cookie_secure: true in production and terminate TLS before the app so Secure cookies survive the hop.

Turn the policy off (nubit_admin.auth.csrf_protection: false) only if an application enforces CSRF some other way in front of it (e.g. a strict Origin allowlist at the edge) — turning it off does not weaken the existing SameSite=Strict cookies, it only removes this extra check.

Session profile (GET /api/me)

The React SessionProvider calls this on boot. Default response:

timeZone is the zone the frontend must render timestamps in. Storage is always UTC, so without it the client has nothing to format against.

app_profile Extra blocks
internal none (single-org panel)
saas tenant (when TenantContext is set), features (from FeatureCheckerInterface::getEntitlements())
hybrid same as saas — branch/context fields come from a custom MeResponseBuilderInterface

Alias MeResponseBuilderInterface to add application-specific fields without forking the route.

Money

Amounts are exact. Nubit\Platform\Money\Money holds an integer count of minor units plus a currency; nothing in the stack converts it to a float, and every operation that can lose precision demands a rounding mode rather than picking one silently.

That is all the wiring. The property is published as { "amount": "1234.50", "currency": "EUR", "scale": 2, "minorAmount": 123450 } with x-crud.format: money, so @nubitio/crud renders a money field without further configuration.

The amount travels as a string: a JSON number is an IEEE-754 double in every JavaScript runtime, and publishing it as a number would undo the exactness at the last step. minorAmount rides along for clients that want to compute in integers.

Storage is three columns — bigint minor units, currency, scale — because an ERP has to SUM and compare amounts in SQL, and none of that works against a formatted string. The scale is stored rather than derived, so a row stays readable independently of the currency table the application shipped that day.

Mixing currencies throws. So does an amount with more decimals than the currency has, unless a rounding mode says what to do with them.

Time

Timestamps are stored in UTC and displayed in the viewer's zone. Two settings:

enforce_utc overrides Doctrine's datetime_immutable so values are both written and read as UTC. Reading is the half that is easy to forget: the stock type parses the stored string in PHP's default timezone, so a server set to anything but UTC silently shifts every value it loads.

For a per-user or per-tenant zone, implement Nubit\Platform\Time\TimeZoneAwareInterface on the entity that decides. The resolution order is user → tenant → default_timezone → UTC, and the resolved identifier is reported by GET /api/me.

Reading grids that grew

Every grid is small on the day it ships. The ones that stop working are the ones nobody decided anything about: page 4,000 of an offset-paginated table asks the database to fetch and discard 80,000 rows, and the footer's COUNT(*) walks the relation on top of that.

#[GridScale] is where a resource states which of those costs it will pay. API Platform provides the mechanisms; this declares the intent and publishes it as x-grid-scale, so the frontend paginates the way the backend expects.

All five lines are load-bearing. API Platform builds the next-page link as ?id[lt]=…, which is ignored without the RangeFilter — and without a declared order, the cursor walks rows in whatever sequence the database felt like. Either omission makes every page return the same rows, silently. The bundle refuses to boot rather than let that ship.

Sorting by any other column is refused with a 400: a cursor walks one ordered field, so another order makes pages repeat and skip rows. Ignoring the sort would show the user an order they did not ask for; obeying it would show them a wrong page.

exactCount: false (or paginationPartial) drops the COUNT(*). The footer still gets a number — X-Estimated-Count, read from PostgreSQL's planner statistics in one indexed lookup — for unfiltered collections only. A filtered count cannot be estimated, and a number that ignored the filter would be worse than none.

Queued exports (opt-in)

Above the limit — per resource via #[GridScale] — an export becomes a job:

Route Purpose
POST /api/exports/{resource} Queue one, carrying the grid's own query
GET /api/exports What you have asked for
GET /api/exports/{id} Status
GET /api/exports/{id}/file The bytes, streamed. 202 while it runs

Route Nubit\AdminBundle\Export\Message\RunExport to a transport. With the notification module on, the requester is told when it finishes.

Queued exports are XLSX, streamed. They go through openspout/openspout, which appends each row to the sheet as it arrives — measured at 4 MB of peak growth for 50,000 rows. PhpSpreadsheet, which the inline export uses for its styling, totals and validation, builds the entire workbook in memory before writing a byte, so it cannot be the queued writer at any size that matters.

The trade is features: no formulas, no data validation, no totals row. Those stay on the inline export, which is a presentation artifact with a bounded row count; this one is a data dump, and one that opens is worth more than a beautiful one that never finishes. Set queued_format: csv for a writer that needs no dependency at all.

Rows are streamed with toIterable() and detached as they go, so memory stays flat regardless of size.

The requester's row scope is reapplied in the worker. A worker has no session, and an export that dropped scope would hand a warehouse supervisor the whole company in a spreadsheet — asynchronously, with nobody watching. If the account no longer exists, the job fails rather than widening.

Identity lifecycle (opt-in)

Add the public routes to access_control — whoever needs them is by definition unable to sign in:

Second factor. POST /api/auth/totp starts enrolment and returns the secret, an otpauth:// URI and ten recovery codes — the only time any of them is readable. POST /api/auth/totp/confirm puts it in force; until then, scanning a QR and closing the tab cannot lock anyone out. Sign-in then takes a totpCode alongside the password; without one the login answers 401 so the client can prompt.

A code is single-use: a TOTP code stays valid for its whole window, so an observed one would otherwise be replayable for a minute and a half. Recovery codes are stored hashed and consumed when used. DELETE /api/auth/totp and regenerating recovery codes require { "code": "..." } — a stolen access cookie is not enough to turn the second factor off.

Password recovery. POST /api/auth/password/forgot always answers 204, whether or not the address exists — anything else turns the endpoint into a way to test who works at the customer. Requests are counted per identity and per IP. The token is hashed, short-lived and single-use, asking again invalidates the previous one, and completing a reset revokes every session.

Delivery is an event, not a mailer call: listen for Nubit\AdminBundle\Identity\Event\PasswordResetRequested and send it however the product sends things.

Invitations. POST /api/invitations with an email and roles requires ROLE_ADMIN. The roles ride on the token, so the account exists with the right authority from its first second. UserInvited carries the plaintext token for delivery. Reset and invitation passwords must be at least 8 characters, same as change-password.

API keys. POST /api/api-keys returns the key once; afterwards only the prefix is visible. A caller lists, rotates and revokes their own keys; administrators see every key. Creating a key for another username is an admin action. A key authenticates as a principal, so permissions, row scope and the audit trail keep working with no special case — an integration is a user that never types a password. Present it as X-Api-Key, and register ApiKeyAuthenticator alongside JWTAuthenticator in the firewall:

POST /api/api-keys/{id}/rotate issues a replacement and revokes the old one in one step, because done separately that is how an integration ends up either broken or still holding a credential somebody thought was gone.

Sessions. GET /api/auth/sessions lists what is open — device, address, last use — and DELETE /api/auth/sessions/{id} closes one. Revocation is scoped to the owner: a session id is a small integer, and revoking by id alone would let anyone sign anyone else out by counting upwards.

For anything the default user gateway cannot express, alias IdentityUserGatewayInterface and write the three methods it names.

Granular permissions (opt-in)

Needs symfony/expression-language — the module derives security: expressions, and refuses to compile without it rather than failing on the first request.

Permissions are resource.action and are derived from the operations a resource already declares. Adding a Delete() creates invoice.delete; removing the operation removes the permission. Nothing is maintained by hand, because a hand-kept list drifts in the dangerous direction: an operation nobody wrote a permission for stays reachable by everyone.

bin/console nubit:permissions:list prints the catalogue (--json for tooling).

Deny by default. With enforce_by_default, every operation that declares no security: gets the expression its permission implies. An explicit security: always wins — inference fills the gap left by whoever did not think about authorization, it never overrules whoever did.

Roles are data. The Role entity is an ApiResource, so the administration screen is the CRUD engine reading the same contract as everything else:

ROLE_* stays the identity, so an application already built on Symfony roles keeps working and adopts granularity where it needs it.

Row scope answers "which of our data is yours", which tenancy does not:

claim names an accessor on the user (getWarehouses()). Null means unscoped — a manager. An empty list means scoped to nothing, because an account nobody finished setting up is far more common than a deliberate grant of everything. The restriction is applied inside the query, for collections and items: restricting only the list leaves every hidden row one guessed identifier away.

Limits are checked in the voter, against the Money the record carries:

Comparing across currencies is refused rather than converted. A user holding a permission through several roles gets the most permissive limit — adding a role should feel like adding authority.

GET /api/me publishes the effective permissions and limits. That decides what the UI offers, never what the API allows: the same permissions are enforced in the voter, so a client that ignores the list gets a 403.

Issued documents (opt-in)

An issued document is a record, not a rendering. Reprinting returns the stored bytes; a template change six months from now must not rewrite invoices that are already in someone else's hands. A correction emits a new document referencing the one it replaces, and both stay readable.

Templates are autoconfigured through DocumentTemplateInterface — no tag needed.

Route Purpose
POST /api/documents/{resource}/{id} Issue. Idempotent: a second call returns the same document.
POST /api/documents/{resource}/{id}?reissue=1 Emit a correction superseding the current copy.
GET /api/documents/{id}/file The exact issued bytes. 202 while a queued render is pending.
GET /api/documents/{resource}/{id} Every copy ever issued, newest first.

{resource} is the published resource segment (invoices), never a class name: accepting a class name from a URL would let a caller name any class in the application.

Each row stores a SHA-256 of the bytes, so a later reader can prove the archived file is the file that was issued.

Rendering goes through DocumentRendererInterface. WeasyPrint is the bundled implementation; replace that one service to render through Gotenberg, a headless browser or a print service, and every issuing rule stays intact.

With async: true, issuing returns a pending document and a Messenger worker completes it — route Nubit\AdminBundle\Document\Message\RenderDocument to a transport. A redelivered message never re-renders a document that is already ready.

The resource publishes x-printable, so @nubitio/crud's PrintButton renders without further configuration.

Spreadsheet import (opt-in)

Uploading never writes business data. The file is analysed and a report says what applying would do — which rows insert, which update, and exactly what is wrong with the ones that would fail, by the line number the user sees in their spreadsheet. Only then can it be applied, in one transaction.

Route Purpose
POST /api/imports/{resource} Upload (multipart field file) and dry-run.
GET /api/imports/{id} The report.
PATCH /api/imports/{id} Correct the column mapping and re-run the dry run.
POST /api/imports/{id}/confirm Apply. Refused while any row is invalid.

The natural key is what makes a corrected file safe to re-upload: without it, fixing one row and uploading again duplicates every row that was already fine.

CSV (delimiter detected, BOM stripped) and XLSX (read-only, streamed) are read out of the box. Values are coerced strictly — a cell that does not clearly mean what the column needs becomes a row error rather than a zero or an epoch date. 31/02/2026 is rejected rather than rolled into March.

Numbers deserve a note. 1.234,56 and 1,234.56 are the same amount written for different readers and both are handled, but 1,234 is genuinely ambiguous and reading it wrong moves an amount by a factor of a thousand. In auto it is refused with an actionable message; send numberFormat: dot|comma to state the file's convention.

Relations are out of scope in this version: a column naming a supplier still needs application code.

The resource publishes x-importable, which @nubitio/crud's ImportPanel renders from.

Embedded lines (master-detail forms)

Line entities that belong to a parent document use #[EmbeddedLines] on the Doctrine class — the bundle registers GET /api/{lines} returning a plain JSON array for SmartCrud formDetail reload (no Hydra envelope, no custom controller).

Import embedded line routes in addition to the bundle routes:

On the parent processor, extend AbstractEmbeddedLinesProcessor to bind lines before persist. Frontend:

Runtime config (GET /api/runtime-config, opt-in)

Separate from /api/me: UI flags, defaults, capabilities, onboarding state — free-form JSON defined by the application. Enable the route, implement the provider, alias it:

On the React side, useRuntimeConfig() from @nubitio/react-admin fetches the payload (RuntimeConfig is Record<string, unknown> — type it per app). Disabled by default so internal skeletons work with zero config.

Configuration (defaults shown)

Analytics outbox (opt-in)

Enable nubit_admin.analytics.enabled, generate a Doctrine migration, and publish typed events through AnalyticsPublisher. The bundle maps nubit_analytics_outbox; its provider calls persist() but deliberately does not call flush(), so the event commits atomically with the business change:

Call the publisher before the application's normal flush. Use a stable event ID. The table has a unique constraint as the durable idempotency boundary. Payloads are sanitized before persistence; exception text and original DTOs are never stored. Product/marketing consent and the final delivery provider remain application extension points.

Alias AnalyticsDeliveryProviderInterface to the vendor adapter and route the ID-only message asynchronously:

Run nubit:analytics:dispatch-outbox on a short schedule. Concurrent workers lock each row; duplicate messages become no-ops after delivery. Provider failures are committed with an exponential next-attempt time before being rethrown to Messenger. The built-in unavailable provider fails closed until the application replaces the interface alias. Schedule nubit:analytics:purge-outbox daily to remove delivered rows past retention; undelivered rows are never purged by this command.

Alternatively set delivery_endpoint to use the built-in webhook adapter. It sends a vendor-neutral JSON envelope with a bearer token, accepts HTTPS by default, never reads or stores provider response bodies, and reports only the HTTP status on failure. A small gateway can translate this envelope to PostHog, Segment or an internal warehouse API.

Audit trail (opt-in)

audit.enabled: true records field-level before/after diffs for entities marked #[Nubit\ApiPlatform\Attribute\Auditable] (creates, updates, deletes — captured from the Doctrine change set, written to nubit_audit_log in the same request, attributed to the authenticated user). Serve them to the AuditTrailPanel in @nubitio/react-admin:

GET /api/audit-trail/{resource}/{id} returns newest-first entries in the panel shape: [{ id, timestamp, user, action, changes: { field: { before, after } } }]. Relations collapse to their id; ignored_fields are excluded from diffs; collection contents are not audited. Create the table with a migration and schedule bin/console nubit:audit:purge.

Media library (opt-in)

media.enabled: true exposes a ready-made upload pipeline matching fileField() / imageField() in @nubitio/react-admin (instant upload — the form submits only the media IRI):

Storage is local disk by default (zero config). For S3 (or anything Flysystem speaks), point media.storage.filesystem at a FilesystemOperator service — e.g. with oneup/flysystem-bundle:

To serve direct S3/CDN URLs instead of streaming through PHP, implement Nubit\AdminBundle\Media\MediaUrlResolverInterface and alias it in services.yaml. Create the table with a migration (doctrine:migrations:diff picks up nubit_media once enabled). Reference uploads from your entities as a plain ManyToOne to Nubit\AdminBundle\Media\Entity\Media.

Spreadsheet export (opt-in)

export.enabled: true registers xlsx as an API Platform format and installs the machinery. Resources then opt in one at a time:

An export streams every row matching the query, with pagination removed — a far wider read than the paginated grid the same user sees. That is why the attribute is required rather than assumed: a resource holding payment schedules or personal data should not gain a whole-table dump because a sibling resource needed a spreadsheet. Without #[Exportable] the format is removed from the resource's operations, so API Platform answers 406 Not Acceptable and the OpenAPI document does not advertise it.

Unlisted, the attribute covers the resource's GET operations only. Pass operations: to narrow it further:

Operation security: expressions still apply — the export is the same operation, answered in another format.

The encoder serializes whatever the normal normalizer chain already produced, so groups, x-crud hints and computed properties apply unchanged: a collection becomes one row per item, an item becomes a one-row workbook. Anything else (an empty result, a scalar) encodes as an empty workbook rather than failing. A Content-Disposition filename is added automatically (products-2026-08-20.xlsx) so browsers download instead of rendering bytes.

Requires phpoffice/phpspreadsheet with ext-zip and ext-gd — the package is a suggest, so enabling the feature without it throws at container build with a message naming what to install.

Frontend counterpart: permissions: { canExport: true } on defineResource renders the grid's Export button, which exports every row matching the current filters and sort (pagination dropped), not the page on screen. The two gates are independent and both default to off — the button hides an endpoint the user could still call, so #[Exportable] is the one that actually restricts access.

For hand-built exports with column control, totals rows and cell validation, use Nubit\Platform\Export\XlsExporter and friends directly instead.

SSO / OpenID Connect (opt-in)

oidc.enabled: true adds an authorization-code + PKCE login against any OpenID Connect-compliant IdP (Okta, Entra ID, Google Workspace, Auth0, Keycloak…). Integration is by issuer discovery, so there is no provider-specific SDK:

Two things the bundle deliberately does not decide for you:

resolve(array $claims, OidcProviderConfig $provider): UserInterface decides everything policy-shaped: look up by sub/email, JIT-provision on first login, reject unknown users, map IdP groups to roles. Throw OidcAuthenticationException to refuse.

Notifications (opt-in)

notification.enabled: true registers a channel-agnostic dispatcher. Domain code describes what happened; channels decide how it is delivered:

Dispatch goes through Messenger, so a slow mail server never blocks the request. Route NotificationMessage to a transport in messenger.yaml to make it genuinely async — it runs synchronously otherwise.

Frontend counterpart: useNotifications() and <NotificationPanel> in @nubitio/admin.

Tenant backups (opt-in)

backup.enabled: true registers a PostgreSQL TenantBackupRunnerInterface plus bin/console nubit:tenant:backup <tenant> [--type=full] [--dry-run]:

pg_dump --format=custom, with credentials read from the Doctrine connection rather than re-parsed from DATABASE_URL, invoked through Process with an argument array (never a shell string) and the password passed via PGPASSWORD so it never appears in ps aux. Dumps are written through Flysystem, so "local disk vs S3" is only which filesystem you point it at.

Scope is deliberately narrow: PostgreSQL only (it throws on any other driver instead of writing a partial dump), and there is no backup-history table — the returned id is a timestamp. Implement TenantBackupRunnerInterface yourself for other engines or for a queryable history.

Security audit

bin/console nubit:security:audit lists every POST/PUT/PATCH/DELETE operation with no security: expression. Routes under /api already require ROLE_USER via access_control, so an unguarded operation is not world-open — it is reachable by any authenticated user, whatever their role. That is the right default for most reads and a common accident on writes. --strict exits non-zero, which makes it usable as a CI gate. Always registered; no config.

Clients

Web (@nubitio/core) — works out of the box: login stores HttpOnly cookies; CoreProvider auto-refreshes via auth/refresh.

Android / API — send response_mode: "json" on login (or the X-Client-Type: android header on every auth call):

Refresh with { "refreshToken": "..." } in the body; send Authorization: Bearer <token> on every request.

Extension points

Hook Purpose
MeResponseBuilderInterface Shape GET /api/me (session profile for @nubitio/react-admin) — alias your implementation to add branch, currency, or domain context
RuntimeConfigProviderInterface Shape GET /api/runtime-config (UI flags, defaults, capabilities) — alias your implementation; enable with runtime_config: true
TokenClaimsProviderInterface Add claims (user id, role, branch, tenant) to JWTs and shape the login response user payload — alias your implementation over the default
LoginResponseDecoratorInterface Attach extra cookies to the web login/refresh response (e.g. a Mercure subscriber JWT) — autoconfigured by interface
RefreshTokenStoreInterface Swap the Doctrine store for Redis/other
OidcUserResolverInterface Map verified ID token claims to an app user (lookup, JIT provisioning, role mapping) — required when oidc.enabled
NotificationChannelInterface Extra delivery channels (Slack, SMS, push) — autoconfigured by interface
TenantBackupRunnerInterface Replace the PostgreSQL/pg_dump runner for other engines or a queryable history
MediaUrlResolverInterface Emit direct S3/CDN URLs for media instead of the streaming route
GridVirtualFieldInterface Grid fields without ORM mapping — autoconfigured by interface
Nubit\Platform tenant/feature/quota aliases Override for multi-tenant SaaS

License

MIT


All versions of admin-bundle with dependencies

PHP Build Version
Package Version
Requires php Version >=8.3
api-platform/core Version ^4.3.12
doctrine/orm Version ^3.0
firebase/php-jwt Version ^7.0
league/flysystem Version ^3.0
nubitio/api-platform Version ^1.0
nubitio/platform Version ^1.0
psr/cache Version ^3.0
psr/log Version ^3.0
symfony/cache-contracts Version ^3.0
symfony/config Version ^7.4 || ^8.0
symfony/console Version ^7.4 || ^8.0
symfony/dependency-injection Version ^7.4 || ^8.0
symfony/doctrine-bridge Version ^7.4 || ^8.0
symfony/event-dispatcher Version ^7.4 || ^8.0
symfony/http-client-contracts Version ^3.0
symfony/http-foundation Version ^7.4 || ^8.0
symfony/http-kernel Version ^7.4 || ^8.0
symfony/messenger Version ^7.4 || ^8.0
symfony/mime Version ^7.4 || ^8.0
symfony/password-hasher Version ^7.4 || ^8.0
symfony/routing Version ^7.4 || ^8.0
symfony/security-bundle Version ^7.4 || ^8.0
symfony/security-core Version ^7.4 || ^8.0
symfony/security-http Version ^7.4 || ^8.0
symfony/serializer Version ^7.4 || ^8.0
symfony/string Version ^7.4 || ^8.0
symfony/translation-contracts Version ^3.0
symfony/uid Version ^7.4 || ^8.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package nubitio/admin-bundle contains the following files

Loading the files please wait ...