Aller au contenu

omnibase/classroom

A teacher's open classroom for omnibase: levels and subjects, learning sequences and their sessions, idea cards, resources to download (free, for members, or sold through omnibase/marketplace).

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

A teacher's open classroom for the sites built on omnisense/omnibase: what a colleague passing by may pick up, organised the way the French teachers' sites are - by level, by subject, by purpose - and built on omnibase's own Thread and Taxon instead of a blog engine. It is a Symfony bundle (Base\Classroom\ClassroomBundle) on the core 3 or 4, with its back office on omnibase/admin; with omnibase/marketplace it sells resources, with omnibase/consent its frames wait for the visitor's yes.

Entity Is a What
Level Taxon a class level, in a tree of cycles: Cycle 2 › CP, CE1, CE2
Subject Taxon a discipline and its domains: Français › Lecture, Grammaire
Purpose Taxon "pour la classe": rituals, displays, autonomy, games
Sequence Thread a learning sequence: objectives, competences, period (P1-P5), material, cover, resources, and its sessions
Session Thread, child of a sequence one session: its place, duration, aim, material and course
Card Thread an idea card: a picture, a few lines, one to three links - the home page's fan
Resource - a file to download (PDF, document, spreadsheet, archive): free, for members, or paid
Product\ResourceOffer omnibase/marketplace Product a paid resource for sale
Entitlement - the right to download a paid resource, written when its order is paid

Levels, subjects and purposes are the threads' taxa, so a sequence is "CP and CE1 · Allemand" and every list filters by them. The text of a sequence, a session or a card is EditorJS, autosaved, co-edited live when the core's relay runs. Resources live outside public/ and are served through signed, short-lived links.

How this manual is organised

Proposer une modification docs/index.md

Installation

Requirements

What composer.json requires: PHP 8.2 or later, doctrine/orm 2.15 or 3, omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin, phpoffice/phpspreadsheet 1.23 to 5, and symfony/config, dependency-injection, form, http-kernel and security-bundle 6, 7 or 8. It suggests omnibase/marketplace (selling resources) and omnibase/blog (comments under a sequence); it works with omnibase/consent when the site has it.

The bundle

// config/bundles.php
return [
    // ...
    Base\BaseBundle::class => ['all' => true],
    Base\Admin\AdminBundle::class => ['all' => true],
    Base\Classroom\ClassroomBundle::class => ['all' => true],
];

ClassroomBundle aliases Base\Classroom\Entity\* to App\Entity\Classroom\* and the repositories to App\Repository\Classroom\* (unless the application declares its own), and registers Compiler\EmbedsConsentPass. Its templates/ is @Classroom, its public/ bundles/classroom/.

The routes

# config/routes.yaml
classroom_controller:
    resource: "@ClassroomBundle/src/Controller/Client"
    type: attribute
    prefix: /

The paths are French (The pages); /boutique exists only with omnibase/marketplace's quick order.

The storage

The files go to the flysystem storage local.classroom (the Resource::$file uploader), which the host defines, outside public/ (classroom.storage_dir is not read: the storage's own directory is what counts):

# config/packages/flysystem.yaml
flysystem:
    storages:
        local.classroom:
            local:
                directory: '%kernel.project_dir%/var/storage/classroom'

Configuration

# config/packages/classroom.yaml (every key optional)
classroom:
    accel_prefix: ''
    download_ttl: 600
    preview_rows: 30
    members_role: ROLE_USER
    per_page: 24
    from_email: null

The schema and the public files

bin/console doctrine:migrations:diff
bin/console doctrine:migrations:migrate
bin/console assets:install

Tables classroom_sequence, classroom_session, classroom_card (joined to the core's thread table), classroom_resource, classroom_resource_taxon, classroom_sequence_resource, classroom_entitlement, and the taxa's discriminator entries; the stylesheet bundles/classroom/css/classroom.css.

What the layout provides

The pages extend layout1.html.twig and fill its title, description, content and stylesheets blocks.

Proposer une modification docs/installation.md

Levels, sequences, cards and resources

The taxa

Level, Subject and Purpose extend the core's Base\Entity\Thread\Taxon (a translated label and description, an icon, a slug, a parent and children):

Taxon Discriminator Adds
Level classroom_level position (youngest first), short ("CP"; the label when empty), color; isCycle() - no parent: a cycle holds levels
Subject classroom_subject position, color (the parent's when empty)
Purpose classroom_purpose position

The threads (sequences, cards) and the resources keep their levels, subjects and purposes in one association, taxa, read and written by kind (ClassifiedTrait, TaxaOfKindTrait): getLevels() (by position), getSubjects(), getPurposes(), and setLevels(), setSubjects(), setPurposes(), each replacing the taxa of its kind and leaving the others; a value of another kind is ignored. isMultiLevel(), getLevelsLabel() ("CP · CE1").

Sequence

Extends Thread (classroom_sequence); title, headline, excerpt, the presentation (content), slug, state and publishedAt are the thread's.

Property Column Meaning
periodValue period, string(2), nullable Period: p1 to p5, the five periods of the French school year; getPeriod()
durationMinutes integer, nullable the whole sequence; null: the sum of its sessions
objectives, competences json lists; …Text accessors take one per line
materials text
cover upload, image, 8 MB
featured boolean first among the latest
resources many-to-many Resource (classroom_sequence_resource) the files to download
embeds json what it frames: {label, url}; embedsText takes "Label | https://..." per line (only http(s) addresses kept)

getSessions() - its children that are sessions, by position then id.

Session

Extends Thread (classroom_session), a child of its sequence (setSequence() sets the parent): position (1 at least), durationMinutes, materials (a list, one per line). Its headline is its aim, its text the course. Its link is the sequence's page at #seance-<position>. A session created in the back office is published.

Card

Extends Thread (classroom_card): image (8 MB), links (up to the author: {label, url}, "label | url" per line in linksText; a line without label is its own label), featured (on the fan), position, icon (a Font Awesome class, big on a card without picture, a badge on one with), color, embeds, and its levels and subjects.

Resource

classroom_resource:

Property Meaning
title (not blank), slug (unique, from the title), description
file the upload on the private storage local.classroom, 64 MB: PDF, zip, Office and OpenDocument files, CSV and the like
filename its name when saved ("kangourou-ce1.xlsx"); else the slug and the extension
mimeType, size kept when the file is saved from the back office
preview a picture of it, on the public storage, 8 MB
visibilityValue visibility column: free, members, paid (Visibility); getVisibility(), isFree(), isPaid()
published true
taxa its levels and subjects (classroom_resource_taxon)
sequences the sequences it belongs to
downloads counted at each full download
owner, createdAt, updatedAt

getExtension() (from the name, else from the type), isSpreadsheet() (xlsx, xls, xlsm, csv, ods), hasFile().

Entitlement and ResourceOffer

Entitlement (classroom_entitlement, unique on user and resource): the buyer, the resource, the order's reference, downloads, createdAt. It never expires.

Product\ResourceOffer extends omnibase/marketplace's Product (discriminator classroom_resource_offer): its resource; not shippable; one at most per order.

Repositories

Repository Methods
SequenceRepository findPublishedPage(int $page, int $perPage, array $taxa = [], ?string $period = null) (newest first), countPublished(), findLatest(int $limit = 3) (featured first), findOnePublished(), search() (title, excerpt, text), countDrafts()
CardRepository findFeatured(int $limit = 9) (the fan: featured first, by position, newest), findPublished(array $taxa = [], int $limit = 200), findOnePublished(), search() (title, headline, text)
ResourceRepository findPublished(array $taxa = [], int $limit = 200) (newest first), findOnePublished(), search() (title, description), sumDownloads()
EntitlementRepository findForUser(), findOne(?object $user, Resource), countSince()

$taxa are slugs, all required. A published thread is PUBLISH with a publishedAt past or none. Base\Classroom\Service\Taxa: cycles(), levels() (the levels alone), subjects(), purposes() (the top ones, in order), and one by slug: level(), subject(), purpose().

Proposer une modification docs/concepts.md

The pages

Route Path Sitemap Page
classroom_index /classe 0.8, weekly the cycles, subjects and purposes, six latest sequences, six cards of the fan
classroom_level /classe/{level} or /classe/{level}/{subject} a level, and a subject
classroom_subject /matiere/{subject} a subject
classroom_purpose /pour-la-classe/{purpose} a purpose
classroom_sequences /sequences 0.7, weekly the sequences alone
classroom_sequence /sequences/{slug} a sequence, its sessions, four related ones of its subjects
classroom_cards /cartes 0.7, weekly the cards alone
classroom_card /cartes/{slug} a card, the previous and the next of the fan
classroom_resources /ressources 0.7, weekly the resources alone
classroom_resource /ressources/{slug} a resource, its preview, its offer when for sale
classroom_download /ressources/{slug}/telecharger the download's first step
classroom_download_file /ressources/fichier/{id}/{filename} the signed file
classroom_search /rechercher?q= sequences, cards and resources holding the term (3 characters at least)
classroom_shop /boutique the resources for sale (The shop)

An unknown slug is a 404.

The listings

The level, subject, purpose, sequences, cards and resources pages are one listing (@Classroom/client/listing.html.twig), filtered by the taxa in the path (all required):

Query
?periode=p1 ... p5 the sequences of that period
?voir=all, sequences, cards, resources what is shown; all (the taxa pages) shows the sequences of the page, 8 cards and 8 resources
?page= the sequences' page, classroom.per_page (24) a page

The page gets the level, subject and purpose chosen, every level, subject and purpose (the pills), the periods and the counts for the pager.

Proposer une modification docs/usage-pages.md

Downloads and embeds

Signed downloads

A resource's file is downloaded through a signed link (omnisense/omnibase's Base\Service\DownloadLinks): ResourceController signs classroom_download_file for classroom.download_ttl seconds once ResourceVoter let the visitor through, and checks the signature when the file is asked for; after a quick order, QuickOrderFilesListener signs a link for a day. The bundle no longer has a DownloadLinks of its own.

Who may download

Base\Classroom\Security\ResourceVoter decides CLASSROOM_DOWNLOAD on a Resource: an unpublished one only for ROLE_ADMIN; then a free one for anyone, a members' one for classroom.members_role (ROLE_USER), a paid one for ROLE_ADMIN or whoever holds an Entitlement to it. classroom_may_download(resource) asks it in Twig.

The download, in two steps

  1. /ressources/{slug}/telecharger (classroom_download): a resource without file is a 404; refused, an anonymous visitor goes to the sign-in (back here after), a signed-in one back to the resource's page with the flash "paid only" or "members only"; allowed, a redirect to classroom_download_file signed for classroom.download_ttl seconds (with the entitlement's id, when there is one).
  2. /ressources/fichier/{id}/{filename} (classroom_download_file): an invalid or expired signature is a plain 403 (signing in would not help); a file missing from the storage a 404. A GET without Range counts one download on the resource and on the entitlement. The file is sent as an attachment under its saved name - by nginx when classroom.accel_prefix is set (X-Accel-Redirect: <prefix>/<path under the storage>), else by PHP (BinaryFileResponse, private).

The spreadsheet preview

Base\Classroom\Service\SpreadsheetPreview::sheets(Resource): for a spreadsheet (xlsx, xls, xlsm, ods through PhpSpreadsheet, data only; csv read directly - semicolons when the first line has more of them than commas), the first classroom.preview_rows (30) rows of each sheet, empty rows dropped, with whether there are more; cached a day, until the resource changes. A file that cannot be read gives nothing.

What a sequence or a card frames

The "Intégrations" of a sequence or a card (one address per line, "Label | https://…") are framed by omnisense/omnibase's Twig function embed_url(): Canva, Padlet, YouTube, Vimeo, LearningApps, Genially and Google Slides are read from the address; any other site is asked for its player (embed/embed, oEmbed) and framed when it has one, a plain link otherwise. classroom_embed() and Base\Classroom\Service\Embeds are gone; @Classroom/client/_embeds.html.twig uses embed_url() (embed.open.other labels a provider not known by name).

A frame brings its provider's cookies (Canva, Padlet, YouTube, Vimeo...). On a site with omnibase/consent, it waits for the visitor's yes to the EMBEDS feature - the name omnibase/video's platform frames use: the iframe carries its address in data-consent-src and has none until then, and a sentence (embed.consent.text, the provider's name) stands in its place with a button that opens the detailed section of the cookie panel (data-consent-open="details"), where EMBEDS is listed; the link to the provider's page stays under the frame. Accepted, the frame loads; refused again, it is emptied.

Compiler\EmbedsConsentPass adds the feature to the ones every site starts from (omnibase/consent's consent.defaults) - on demand: listed on the pages that frame something, never a reason to ask a visitor again - with what these providers were seen to keep on their own domain (Canva's CDI and CL, Vimeo's vuid, the YouTube player's yt-remote-*); its words are translations/consent+intl-icu.*.yaml (features.embeds.*, storage.embeds_*). A site lays its own consent.features.EMBEDS over it, field by field (declared there, it is listed on every page unless it says on_demand: true), and its own words over the bundle's; defaults: false leaves it out. It is not put in the site's consent: configuration, whose features are the site's own list (data-consent-groups). Without omnibase/consent the frames load with the page (classroom_consent() is null).

Proposer une modification docs/downloads-and-embeds.md

The shop

With omnibase/marketplace, a paid resource is sold as a Product\ResourceOffer.

Putting a resource on sale

The resource's Mettre en vente (/admin/resources/{entityId}/sell) finds its offer, or makes one in the first store - titled and described as the resource, a draft - and sets the resource paid; then opens the offer's screen (ResourceOfferCrudController, omnibase/marketplace's product screen plus the resource) for its price and publication.

Selling

/boutique (classroom_shop, loaded only with omnibase/marketplace's QuickOrder) lists the offers for sale whose resource is published and has a file, newest first, sold with the marketplace's quick order: an e-mail address, the payment, no account, no basket. A signed-in member buys in their own name.

Delivering

  • EventSubscriber\OrderPaidSubscriber (on OrderPaidEvent): for each item of the order that is a resource offer, an Entitlement for the customer (one per resource and buyer: a webhook and a redirect confirming the same order write it once); then a mail (@Classroom/email/files.html.twig, from classroom.from_email) with the link of the order's page (QuickOrder::doneUrl()). An offer without resource is logged as an error; a mail that fails is logged, never a reason to undo the confirmation.
  • EventSubscriber\QuickOrderFilesListener (on QuickOrderDoneEvent): the quick order's page, reached by its own signed link, renders @Classroom/client/thanks.html.twig with the files - once paid, each behind a signed download link valid a day.

Both are loaded only with omnibase/marketplace (and its quick order for the second).

Proposer une modification docs/usage-shop.md

In a host page

Twig functions and filters

Base\Classroom\Twig\ClassroomExtension:

Returns
classroom_cycles() the cycles, each holding its levels
classroom_levels() the levels alone, in order
classroom_subjects(), classroom_purposes() the top ones, in order
classroom_cards(limit = 9) the fan
classroom_sequences(limit = 3) the latest sequences, featured first
classroom_resources(limit = 6) the latest resources
classroom_consent() EMBEDS when the site has omnibase/consent, else null (the frames load at once)
classroom_may_download(resource) the voter's answer
x|classroom_size bytes in French units: 2,4 Mo
x|classroom_minutes 45 min, 1 h 30

Templates

@Classroom/client/: index, listing, sequence, card, resource, search, shop, thanks, and the partials _card, _sequence_card, _resource_card, _pills, _embeds; @Classroom/email/files; @Classroom/admin/widget/overview. An application overrides one in templates/bundles/ClassroomBundle/....

Translations

The domains classroom (the pages, the back office's words), entities (the screens' names) and consent (the EMBEDS feature's words for omnibase/consent's panel), ICU, in French, English and German.

Proposer une modification docs/usage-twig.md

The back office

Loaded when omnibase/admin is installed. The screens open creating, editing and deleting to ROLE_ADMIN (OpenToAdminsTrait: a teacher writes, not only the super-administrator).

CRUD Slug Fields
SequenceCrudController sequences title, state, slug (following the title), publication date, featured, levels, subjects, purposes (one picker per kind), period, duration, headline, excerpt, objectives and competences (one per line), material, the presentation (EditorJS, autosaved and live), embeds, resources, cover; filters state, period, featured
SessionCrudController sessions sequence, position, duration, state, title, slug, aim, material, course; filter sequence
CardCrudController cards title, state, featured, position, slug, headline, levels, subjects, icon, colour, text, links, embeds, picture; filters state, featured
ResourceCrudController resources title, visibility, published, slug, file, saved name, description, levels, subjects, preview, downloads (list); filters visibility, published; Mettre en vente with omnibase/marketplace
ResourceOfferCrudController resource-offers the marketplace's product screen and the resource (with omnibase/marketplace)
LevelCrudController levels omnibase/admin's taxon screen, plus position, short name, colour
SubjectCrudController subjects the taxon screen, plus position, colour
PurposeCrudController purposes the taxon screen, plus position

A sequence, a card or a resource created here has the signed-in user as owner. Saving a resource keeps its file's size, type and - when empty - its name. Only what a record cannot do without is required (a title, a state, a slug; a resource's file, a session's sequence).

The tile classroom_overview (ClassroomOverviewWidgetType): sequences online and in draft, cards online, resources published, downloads, entitlements bought since the first of the month, each linking to its screen.

Proposer une modification docs/back-office.md

Configuration reference

classroom:

Base\Classroom\DependencyInjection\ClassroomConfiguration, dumped with Symfony's YamlReferenceDumper in omnisense/omnibase's harness image:

classroom:

    # Where the resources are (the local.classroom flysystem storage points there).
    storage_dir:          '%kernel.project_dir%/var/storage/classroom'

    # nginx internal location streaming the files (X-Accel-Redirect); empty: PHP streams them.
    accel_prefix:         ''

    # Seconds a signed download link stays valid.
    download_ttl:         600

    # Rows of a spreadsheet shown in a resource's preview.
    preview_rows:         30

    # Who the MEMBERS resources are for.
    members_role:         ROLE_USER
    per_page:             24

    # Sender of the mail a paid order gets (the link to its files); null: the mailer's default sender.
    from_email:           null
Option Type Default Read by
storage_dir path %kernel.project_dir%/var/storage/classroom nothing (see below)
accel_prefix string '' ResourceController
download_ttl int, 30 or more 600 ResourceController
preview_rows int, 1 or more 30 SpreadsheetPreview
members_role role ROLE_USER ResourceVoter
per_page int, 1 or more 24 ClassroomController
from_email string or null null OrderPaidSubscriber

storage_dir is deprecated: it was read by nothing. The files go to the flysystem storage local.classroom, whose directory the site sets in its flysystem.yaml; a site that still sets storage_dir gets Symfony's deprecation notice.

The extension also sets classroom.consent: whether the container has omnibase/consent's extension.

Services

config/services.php autowires and autoconfigures src/ but DependencyInjection/, Entity/, Enum/, Controller/Admin/, Admin/, EventSubscriber/ and the bundle class; Controller/Client/ is tagged controller.service_arguments (ShopController only with omnibase/marketplace's QuickOrder); with omnibase/admin, Controller/Admin/ (ResourceOfferCrudController only with omnibase/marketplace's Product) and Admin/; with omnibase/marketplace's OrderPaidEvent, EventSubscriber/ (QuickOrderFilesListener only with its QuickOrderDoneEvent).

Compiler\EmbedsConsentPass adds EMBEDS to omnibase/consent's consent.defaults: on demand, with the storage these providers keep - Canva's CDI (a year) and CL (60 days), Vimeo's vuid (two years), the YouTube player's yt-remote-* (local storage). Another bundle's EMBEDS keeps its fields; the entries it does not name are added.

Proposer une modification docs/configuration.md

Testing

vendor/bin/phpunit

The tests are units. In omnisense/omnibase's harness, with this checkout and omnibase/consent's:

docker run --rm --network none \
    -v "$PWD":/srv/demo/vendor/omnibase/classroom:ro -v "$PWD/../consent":/srv/demo/vendor/omnibase/consent:ro \
    omnisense/omnibase-harness test classroom

Run on 2026-10-27 with both at HEAD: 27 tests, 1 error.

EmbedsConsentTest takes omnibase/consent's own features from ConsentPanel::features(), not from a path guessed beside a class's file.

File What
DependencyInjection/EmbedsConsentTest without omnibase/consent the frames load at once; with it EMBEDS is one of the features every site starts from; the panel lists EMBEDS with the bundle's words
Templates/EmbedsTemplateTest with consent no frame has an address before the yes; without, the frames load with the page; the placeholder speaks every language; every language has every key
Entity/TaxaOfKindTest a card's levels replaced, its subjects kept; a sequence's three kinds written apart; what is not of the kind ignored; a resource the same way
Controller/CrudFieldsTest, Controller/CrudRequiredFieldsTest one field per property, written through the entity; the slug follows the title; each kind of taxa has its picker; only what the record needs is required
Controller/PeriodFieldTest a new sequence shows no period; sent empty it stays empty; a period chosen is recorded
Controller/AdminWordsTest every screen named in every language; every entity of a screen listed; every key the screens give translated

Saving through the back office is a host's functional test (a site's own).

Proposer une modification docs/testing.md