omnibase/scholar is a researcher's site on omnibase: the publications read
from the open sources and validated by the researcher, the books, the CV,
the teaching, the research themes and projects, with their structured data
and citation exports. Its namespace is Base\Scholar\; it reads the sources
through omnisense/omnischolar (OpenAlex, HAL, Crossref, ORCID, Open Library).
What it adds
Scholar, the person and the profiles that name them in each source;Publication, a work as the site shows it (the sources' record and the site's own fields);Theme, a research theme or project; six CV entries (Position,Degree,Award,Grant,Course,Supervision).scholar:sync, which reads every profile's works, the counts and the CV, and lays them over the site's publications: a new work waits for validation.- Seven pages: the publications (filtered and paged), a publication, the books, the CV, the teaching, the research, a theme; and two export routes (the list or one publication as BibTeX, RIS or CSL-JSON).
- schema.org
Person,ScholarlyArticle,Bookand the others; the line to copy and the exports. - Twig functions for a host's home page.
- Nine back office screens open to the site's administrator, a validation tile.
- English, French and German messages (domain
scholar).
Pages
Propose a change docs/index.md
Installation and configuration
composer require omnibase/scholar:dev-main omnisense/omnischolar omnischolar/openalex omnischolar/hal omnischolar/crossref omnischolar/orcid
// config/bundles.php
Omnischolar\Bridge\Symfony\OmnischolarBundle::class => ['all' => true],
Base\Scholar\ScholarBundle::class => ['all' => true],
# config/routes.yaml
scholar_controller:
resource: "@ScholarBundle/src/Controller/Client"
type: attribute
prefix: /
The sources (omnisense/omnischolar)
The names a scholar's profiles use are the configured sources:
# config/packages/omnischolar.yaml
omnischolar:
sources:
openalex: { factory: openalex, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
hal: { factory: hal }
crossref: { factory: crossref, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
orcid: { factory: orcid }
openlibrary: { factory: openlibrary, options: { mailto: '%env(OMNISCHOLAR_MAILTO)%' } }
merger:
preferences: { abstract: [hal, openalex], pdfUrl: [hal, openalex] }
The bundle (every key optional)
# config/packages/scholar.yaml
scholar:
max_per_source: 2000 # works read from one source for one scholar
metrics_source: openalex # whose profile gives the counts (h-index...); ~ for none
cv_source: orcid # where the CV lines are read; ~: typed only
per_page: 100 # publications per page
jsonld: true # Person, ScholarlyArticle, Book on the pages
abstracts_in_exports: false
Then the tables (scholar_scholar, scholar_publication,
scholar_publication_theme, scholar_theme, scholar_position,
scholar_degree, scholar_award, scholar_grant, scholar_course,
scholar_supervision):
bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate
bin/console assets:install # public/css/scholar.css
The scholar
In the back office (Identity and sources), or in the fixtures:
$scholar = (new Scholar('Keitaro Nakatani'))
->setOrcid('0009-0005-1387-7295')
->setProfiles([
'openalex' => ['A5108007452'],
'hal' => ['Keitaro Nakatani'], // by the name he signs: by ORCID HAL finds only the deposits that carry it
'crossref' => ['Keitaro Nakatani'],
]);
A profile names a configured source and one or several authors there: an
OpenAlex id, an ORCID, an idHAL, a name - what that source's works() reads
(omnisense/omnischolar's docs/sources.md).
The cron container
30 2 * * 1 php /srv/app/bin/console scholar:sync >> /srv/app/var/log/cron.scholar-sync.log 2>&1
No Symfony Scheduler: the sites' cron container runs it.
Requirements
- PHP 8.2 or later;
- doctrine/orm 2.15 or 3;
- omnisense/omnibase 3.0 or 4.0, omnibridge/symfony 1.x, omnibase/admin;
- omnisense/omnischolar 1.x, and the sources the site reads (suggested: omnischolar/openalex, omnischolar/hal, omnischolar/crossref, omnischolar/orcid, omnischolar/openlibrary);
- Symfony 6.4, 7 or 8 (config, console, dependency-injection, http-kernel, string).
Suggested as well: omnibase/agenda, for a scholar's talks and lectures as agenda events.
Propose a change docs/installation.md
Concepts
The scholar
Base\Scholar\Entity\Scholar (scholar_scholar) is the person the site is
about. The pages and the Twig functions read the first one.
| Field | Meaning |
|---|---|
name (required), givenName, familyName, honorific |
getFullName() is the honorific and the name: "Dr. Monica Neagoy" |
jobTitle, affiliation, affiliationUrl |
for the CV and schema.org |
orcid |
0000-0000-0000-000X |
profiles |
source name => the authors there (an OpenAlex id, an ORCID, an idHAL, a name); typed in the back office one source: author a line (getProfilesText(), setProfilesText()) |
sameAs |
other pages about the person, one a line |
keywords |
what they work on (schema.org knowsAbout) |
portrait, biography |
an image (16 MB); getBiographyParagraphs() |
autoApprove |
the works read go online without validation |
fromYear |
the sources are asked for the works from that year on |
metrics |
the counts of the metrics source (works, citations, h-index, i10-index) |
lastSyncAt, lastSync |
what the last run did (rememberSync()) |
getIdentityUrls() gathers the sameAs pages, the ORCID page, the OpenAlex
author pages (ids like A5108007452) and the HAL CVs (an idHAL, not a name).
A publication
Base\Scholar\Entity\Publication (scholar_publication) is a work as the site
shows it. It holds two kinds of fields:
- the sources' record:
work(omnisense/omnischolar'sWork, as JSON: the abstract, the sources, the identifiers, the editions, the PDF and the page the sources give), with its columnstitle,type,year,venue,authors,doiandcitations, refreshed by every run (refresh($work)), and what the merge recognises it by (matchKeys,fingerprint,fetchedAt,lastSeenAt); - the site's own:
status,manual(typed by hand: its columns are the site's),hidden,pinned(first on the list),selected(the selection),pdf(an upload, 64 MB),pdfUrl,buyUrl,publisherUrl,cover(16 MB),note,themes.
Its type is one of omnisense/omnischolar's WorkType values: article,
book, chapter, conference, preprint, thesis, report, dataset,
software, review, editorial, other (an unknown value becomes other).
| Method | Returns |
|---|---|
isShown() |
validated and not hidden: on the site |
approve(), reject(), isPending(), isPublished() |
the status (PublicationStatus: pending, published, rejected) |
getCoverUrl() |
the cover uploaded, else the sources' (Open Library's) |
getFullTextUrl() |
the PDF uploaded, the address given, else the sources' |
getLink() |
the publisher's page given, else the DOI's address, else the sources' page |
isOpenAccess() |
the sources say so, or there is a full text to offer |
getSlug() |
the title's slug, 80 characters at most, publication for none |
getAbstract(), getIdentifiers(), getHalId(), getArxivId(), getIsbns(), getEditions(), getSources() |
read from work |
toWork() |
the publication as a Work, with the site's own fields over the sources' (what the exports write) |
Themes and projects
Base\Scholar\Entity\Theme (scholar_theme) is a research theme or a project
(ThemeKind: theme, project; isProject()): title, unique slug,
summary, description (getParagraphs()), image, start and end years
(getYears()), funder, address, position, visible. Publications are linked
to themes (scholar_publication_theme).
The CV
Six entities share Base\Scholar\Entity\Cv\CvEntry: the scholar, title
(required), organization, city, country (two letters), start and end
(YYYY, YYYY-MM or YYYY-MM-DD), description, url, position,
visible, origin (site, or the source it was read from) and
externalKey (the source's key, so a line is read once).
| Entity | Table | Its own fields |
|---|---|---|
Position |
scholar_position |
department |
Degree |
scholar_degree |
thesis, advisor |
Award |
scholar_award |
|
Grant |
scholar_grant |
reference, role, amount |
Course |
scholar_course |
level, volume |
Supervision |
scholar_supervision |
level (SupervisionLevel: phd, master, postdoc, intern, other), subject, coSupervisors, outcome; the title is the student (getStudent()) |
getPeriod() writes "2019–2023", "2019–" for a current one, "2019" for one
year; isCurrent() is started and not ended.
Base\Scholar\Service\Curriculum::sections($names) returns the visible
entries of each section that has some (positions, degrees, awards,
grants, courses, supervisions), by position, the current ones first,
then the latest; a section whose table is not migrated yet is left out.
countHidden() counts the hidden lines (a line read from the CV source waits
hidden).
The three enums
PublicationStatus, ThemeKind and SupervisionLevel are PHP enums mapped
with Doctrine's enumType on string columns; each case names itself in the
scholar domain.
Propose a change docs/concepts.md
Synchronisation and validation
bin/console scholar:sync (--scholar=<id> for one) reads, for every
scholar, every profile's works - every page, through omnischolar's
Collector, merged by its Merger (same DOI, ISBN, arXiv id, HAL id, then
title and year) - and lays them over the publications the site has:
| The work | What happens |
|---|---|
| unknown to the site | a new Publication, pending: nothing shows before it is validated (unless the scholar has Publish without validation) |
| known (any identifier ever seen for it, else its title and year) | its sources' record and columns refreshed; nothing the site added moves |
| rejected once | refreshed, still rejected: a homonym's work never comes back |
typed by hand (manual) |
found, its identifiers remembered; its columns are the site's and stay |
| no longer given by the sources | left as it is |
A source that is down or rate limited is named in the summary
(incomplete) and the others still answer; nothing is taken for "gone".
The same run reads the counts of the metrics source's profile
(scholar.metrics_source: works, citations, h-index, i10-index, as OpenAlex
computes them) and the CV source's lines (scholar.cv_source: ORCID's
employments, education, distinctions - each once, by a key of its own;
a new line waits hidden).
What the run did is kept on the scholar (lastSyncAt, lastSync) and shown
in the widget.
Calling it yourself
$summary = $synchronizer->sync($scholar); // read the sources, then merge, flush
$summary = $synchronizer->merge($scholar, $works); // lay Works over the site's (persisted, not flushed)
$summary->created; $summary->updated; $summary->incomplete;
merge() is what a test or an import of a file calls.
Validating
The dashboard's scholar_pending widget lists the newest pending works with
two buttons; the list Publications filters on the status and has the same
actions on each row and record. Publication::approve(), reject().
Pages, Twig functions and the host's look
| Route | Path | |
|---|---|---|
scholar_publications |
/publications |
pinned first, then by year; ?type=article&year=2024&theme=<slug>&q=words, ?page= |
scholar_publications_export |
/publications.{bib,ris,json} |
what the page lists, as a file |
scholar_publication |
/publications/{id}-{slug} |
the record, its abstract, its editions, the reference to copy, its BibTeX |
scholar_publication_export |
/publications/{id}.{bib,ris,json} |
|
scholar_books |
/books |
the covers (the one uploaded, else Open Library's, else the title set as a cover) |
scholar_cv |
/cv |
the person, the biography, the CV's sections, the selection |
scholar_teaching |
/teaching |
courses and supervisions |
scholar_research |
/research |
themes and projects |
scholar_theme |
/research/{slug} |
a theme's text and its publications |
Only validated, not hidden publications are shown. The publication and theme
pages are in /sitemap.xml (EventListener\SitemapListener).
What is not given yet
A portrait, a biography, a CV, research themes that nobody gave are never
filled with an invented text: @Scholar/client/_draft.html.twig marks the
place as a draft (scholar-draft, its tag Draft).
{% include '@Scholar/client/_draft.html.twig' with {kind: 'biography', message: 'The biography is being written.'} %}
Twig, for the host's own pages
scholar() |
the site's scholar (the first one) |
scholar_selected(n) |
the selection, else the latest |
scholar_latest(n, types) |
the latest shown |
scholar_books(n), scholar_count(types), scholar_themes(kind) |
|
scholar_cv(sections) |
{positions: [...], degrees: [...]} |
scholar_reference(publication) |
"Chocron, L., Nakatani, K. (2024). …" |
scholar_person_jsonld(url, image) |
the Person, for a home page |
type|scholar_type_label(count) |
"Article", "Articles" in the reader's language |
{% include '@Scholar/client/_publication.html.twig' %} renders a
publication as the list does; _theme_card.html.twig a theme.
The look
The templates extend layout1.html.twig and fill title, description,
stylesheets and content. public/css/scholar.css follows the host's
custom properties: --scholar-accent, --scholar-on-accent,
--scholar-ink, --scholar-soft, --scholar-line, --scholar-surface,
--scholar-font-display, --scholar-measure. Templates may be overridden
in templates/bundles/ScholarBundle/.
Propose a change docs/pages.md
Structured data and exports
JSON-LD (Service\JsonLd, scholar.jsonld)
| Page | schema.org |
|---|---|
/cv, and a host's home page through scholar_person_jsonld() |
Person: name, given and family names, honorific, jobTitle, affiliation, ORCID as identifier, sameAs (ORCID, OpenAlex, HAL's CV, the pages typed), knowsAbout |
| a publication | ScholarlyArticle (a journal's Periodical, a conference's Event, a book's as isPartOf), Book (ISBNs, publisher, edition, an Offer to buy it), Chapter, Thesis, Report, Dataset, SoftwareSourceCode; the authors, the scholar's with his ORCID |
/publications, /books |
an ItemList of them |
Citing
Service\Citations::reference() writes the line to copy (authors with
initials, year, title, journal, volume(issue), pages, DOI - or ISBN).
export('bibtex'|'ris'|'csl-json', $publications) writes the files through
omnisense/omnischolar's Export, from what the site shows: a PDF or a
publisher's page given on the site goes into the file.
An example
use Base\Scholar\Entity\Publication;
use Base\Scholar\Entity\Scholar;
use Base\Scholar\Service\Citations;
$scholar = (new Scholar('Keitaro Nakatani'))->setOrcid('0009-0005-1387-7295');
$publication = (new Publication($scholar))
->setTitle('Acid-sensitive photoswitches')
->setType('article')
->setYear(2024)
->setVenue('Chemical Science')
->setAuthors('Lucas Chocron, Keitaro Nakatani')
->setDoi('10.1039/d4sc04973j');
$citations = new Citations();
echo $citations->reference($publication), "\n\n";
echo $citations->export('bibtex', $publication), "\n";
echo $citations->export('ris', $publication), "\n";
Its output, run in omnisense/omnibase-harness with omnisense/omnischolar on the autoloader:
Chocron, L., Nakatani, K. (2024). Acid-sensitive photoswitches. Chemical Science. https://doi.org/10.1039/d4sc04973j
@article{chocron2024acid,
author = {Chocron, Lucas and Nakatani, Keitaro},
title = {{Acid-sensitive photoswitches}},
journal = {Chemical Science},
year = {2024},
doi = {10.1039/d4sc04973j}
}
TY - JOUR
ID - chocron2024acid
AU - Chocron, Lucas
AU - Nakatani, Keitaro
TI - Acid-sensitive photoswitches
T2 - Chemical Science
PY - 2024
DO - 10.1039/d4sc04973j
UR - https://doi.org/10.1039/d4sc04973j
ER -
scholar.abstracts_in_exports: true writes the abstracts into the BibTeX and
RIS files as well.
Propose a change docs/structured-data.md
The back office (omnibase/admin)
CRUD screens, under /admin/scholars/...:
| Screen | Controller | |
|---|---|---|
| Identity and sources | Crud\Scholar\ScholarCrudController |
the person; Sync now on each |
| Publications | Crud\Scholar\PublicationCrudController |
Validate / Reject; the sources' fields read only, the site's editable; New types one by hand |
| Themes and projects | Crud\Scholar\ThemeCrudController |
|
| CV | PositionCrudController, DegreeCrudController, AwardCrudController, GrantCrudController, CourseCrudController, SupervisionCrudController |
a line read from ORCID waits hidden |
Who writes
These screens are the site's administrator's (ROLE_ADMIN: the researcher
whose site it is), not the super-admin's only - each controller carries
omnibase/admin's #[OpenToAdmins]: creating, editing and deleting, and the
screens' own buttons (approve, reject on a publication, sync on the
scholar). An application's CRUD extending one of them is opened too, and may
close an action again in its configureActions()
(->setPermission(Action::DELETE, 'ROLE_SUPERADMIN')). The attribute is
omnibase/admin's from 7474f85.
// DashboardController
yield MenuItem::block('scholar_pending', 'Publications à valider', 'fa-solid fa-book-open')->setSize(4);
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Publication::class, 'Publications', 'fa-solid fa-book-open');
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Scholar::class, 'Identité et sources', 'fa-solid fa-id-card');
yield MenuItem::linkToCrud(\Base\Scholar\Entity\Cv\Position::class, 'CV · Postes', 'fa-solid fa-briefcase');
The widget's buttons post to admin_crud_scholars_publications_approve and
_reject with the admin-action-approve / admin-action-reject tokens
(omnibase/admin's #[AdminAction]), and come back to where they were.
A publication's status, a theme's kind and a supervision's level are PHP
enums (PublicationStatus, ThemeKind, SupervisionLevel), mapped with
Doctrine's enumType: on their string columns; in the back office each is a
select of its cases (SelectField::new('status')), which name themselves in
the scholar domain (TranslatableInterface). (They were kept as strings,
with statusValue / kindValue / levelValue accessors for the selects,
until omnibase could put an enum back into an entity's previous state and
build a select from one: same columns, no migration.)
Propose a change docs/admin.md
Configuration reference
The scholar tree
Dumped with Symfony's YamlReferenceDumper from
Base\Scholar\DependencyInjection\ScholarConfiguration:
scholar:
# The most works scholar:sync reads from one source for one scholar.
max_per_source: 2000
# The omnischolar source whose author profile gives the counts (works, citations, h-index); null: none.
metrics_source: openalex
# The omnischolar source the CV is read from (positions, degrees, distinctions), when the scholar has a profile there; null: the CV is typed only.
cv_source: orcid
# Publications listed per page.
per_page: 100
# schema.org Person, ScholarlyArticle and Book on the pages.
jsonld: true
# Write the abstracts into the BibTeX and RIS exports.
abstracts_in_exports: false
| Key | Default | Read by |
|---|---|---|
max_per_source |
2000 | Synchronizer: the most works read from one source for one scholar |
metrics_source |
openalex |
Synchronizer: the source whose author profile gives the counts; null, none |
cv_source |
orcid |
Synchronizer: the source the CV lines are read from, when the scholar has a profile there; null, the CV is typed only |
per_page |
100 | /publications |
jsonld |
true | the pages' schema.org |
abstracts_in_exports |
false | Citations: the abstracts in the BibTeX and RIS files |
Each key is a container parameter: %scholar.max_per_source%,
%scholar.metrics_source%, %scholar.cv_source%, %scholar.per_page%,
%scholar.jsonld%, %scholar.abstracts_in_exports%.
The sources themselves are omnisense/omnischolar's (omnischolar.sources,
Installation and configuration): a profile's source name
is one of them.
Services
| Service | Role |
|---|---|
Base\Scholar\Service\Synchronizer |
isAvailable(), sync($scholar), merge($scholar, $works) |
Base\Scholar\Service\Citations |
reference($publication), export($format, $publications) |
Base\Scholar\Service\JsonLd |
person(), publication(), list() |
Base\Scholar\Service\Curriculum |
sections(), entries(), countHidden() |
Base\Scholar\Service\WorkData |
a Work as the JSON column keeps it and back, every field included; what makes two records the same work (keys(), fingerprint()) |
Base\Scholar\Command\SyncCommand |
scholar:sync, --scholar=<id> |
Base\Scholar\EventListener\SitemapListener |
the shown publications and the visible themes in /sitemap.xml |
Base\Scholar\Twig\ScholarExtension |
the Twig functions and the scholar_type_label filter |
Base\Scholar\Service\PublicationIndex is built by the synchronizer from a
scholar's publications, not taken from the container: it finds the site's
publication for a Work by any identifier ever seen for it (a DOI, a HAL id,
an ISBN of any edition), else by its title and year, unless both carry a DOI
and they differ (two versions of a dataset, an erratum).
Translations
The domain scholar, in English, French and German (ICU format). A site
overrides a message in its own translations/scholar+intl-icu.<locale>.yaml.
Propose a change docs/configuration.md
Testing
Running the suite
From the package's checkout, with its dev dependencies:
vendor/bin/phpunit
Inside a host application:
php vendor/bin/phpunit -c vendor/omnibase/scholar/phpunit.xml.dist
The suite needs omnisense/omnischolar. omnisense/omnibase-harness does not
have it, and its test scholar stops when the kernel boots
(Class "Omnischolar\Export" not found); with omnisense/omnischolar's
packages added to the harness's autoloader (its checkouts mounted), the
suite on this checkout gives:
OK (40 tests, 257 assertions)
What the tests check
| Test | Checks |
|---|---|
Controller\Admin\OpenToAdminsTest |
the administrator writes in the nine screens; anyone else is refused |
Controller\CrudRequiredFieldsTest |
omnibase/admin's CrudFieldsTestCase on the nine screens |
Entity\EnumFieldsTest |
a publication holds its status, a theme its kind, a supervision its level; the columns are strings of the enum's length; each case names itself in the scholar domain |
Service\CitationsTest |
the reference line; the exports carry the site's overrides; the JSON-LD |
Service\SynchronizerTest |
a new work waits to be validated; the site's overrides survive the next run; a work is found again by any identifier ever seen; a rejected work stays rejected and a manual one stays the site's; an auto-approving scholar's works go online at once |
Service\WorkDataTest |
a work comes back whole; a book's editions come back and name it; the authors' line |