Connect a job source to the import
The job import reads jobs from XML sources. Each source is
a PHP class that implements
\JWeiland\ and is tagged with
api.. The extension ships no source, add your classes to a
site package.
Architecture
ImportJobsMhm (console command, argument: storage page UID)
├── ImportService::import() returns an ImportResult
│ ├── for each service tagged "api.model"
│ │ ├── XmlClient: GET request to getApiEndpoint()
│ │ │ (failed request or invalid XML: source failed,
│ │ │ all its jobs are kept)
│ │ ├── every child element of the XML root element = one job
│ │ │ (jobs without vacancy_id or primary location are
│ │ │ skipped with a warning)
│ │ ├── TtAddressService: address of the primary location
│ │ ├── JobService: job record built from getMapping(),
│ │ │ job_areas/job_type resolved or created by title
│ │ ├── ImportRecordValidator: remove invalid emails/URLs
│ │ └── DataHandler: save address and job
│ └── DataHandler: delete imported jobs that a successfully
│ fetched source no longer delivers
└── exit code 0, or 1 if a source or job failed (skipped jobs are
only shown as warning)
Create a source class
<?php
declare(strict_types=1);
/*
* This file is part of the package my-vendor/my-sitepackage.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/
namespace MyVendor\MySitepackage\Import;
use JWeiland\Jobboard\ApiModel\ApiModelInterface;
use JWeiland\Jobboard\ApiModel\ApiModelTrait;
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
#[Autoconfigure(tags: ['api.model'], shared: false)]
final class ExampleJobBoardApiModel implements ApiModelInterface
{
use ApiModelTrait;
private const NAME = 'example';
private const API_ENDPOINT = 'https://example.org/api/jobs.xml';
private const MAPPING = [
'title' => [
'apiPath' => 'title/de',
'isDate' => false,
'default' => '',
],
'description' => [
'apiPath' => 'description/de',
'isDate' => false,
'default' => '',
],
'reference_number' => [
'apiPath' => 'vacancy_id',
'isDate' => false,
'default' => '',
],
'email' => [
'apiPath' => 'application_email',
'isDate' => false,
'default' => '',
],
'link' => [
'apiPath' => 'publication_link',
'isDate' => false,
'default' => '',
],
// Titles, resolved to (or created as) occupational field records
'job_areas' => [
'apiPath' => 'custom_select_4/de',
'isDate' => false,
'default' => '',
],
// Title, resolved to (or created as) an employment scope record
'job_type' => [
'apiPath' => 'custom_select_2/de',
'isDate' => false,
'default' => '',
],
'start_date' => [
'apiPath' => 'start_date',
'isDate' => true,
'default' => 0,
],
'application_deadline' => [
'apiPath' => 'application_deadline',
'isDate' => true,
'default' => 0,
],
'salary_mode' => [
'apiPath' => 'salary_mode',
'isDate' => false,
'default' => 1,
],
'salary_min' => [
'apiPath' => 'salary/min',
'isDate' => false,
'default' => 0,
],
'salary_max' => [
'apiPath' => 'salary/max',
'isDate' => false,
'default' => 0,
],
'vacancy_id' => [
'apiPath' => 'vacancy_id',
'isDate' => false,
'default' => 0,
'prefix' => self::NAME,
],
];
private int $storagePid = 0;
}
The trait
\JWeiland\ implements all
methods of the interface. It needs the class constants
NAME
,
API_ and
MAPPING
and the property
private int $storage, which the trait does not declare.
NAME- Short, unique name of the source. It is used as prefix of the vacancy
ID, for example
example_.4711 API_ENDPOINT - URL of the XML file. It is requested with the TYPO3
Request, so the HTTP settings ofFactory $GLOBALS(timeout, proxy) apply.['TYPO3_ CONF_ VARS'] ['HTTP'] MAPPING- Maps XML values to columns of the table
tx_, see The mapping.jobboard_ domain_ model_ job
Attention
Do not commit endpoints with credentials or internal URLs to public repositories.
Register the class
With the attribute
#, the class is registered automatically, if the
Configuration/Services.yaml of your site package enables
autoconfigure
for this class. Without the attribute, tag the class
in Services.:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
MyVendor\MySitepackage\:
resource: '../Classes/*'
# Only needed without the #[Autoconfigure] attribute
MyVendor\MySitepackage\Import\ExampleJobBoardApiModel:
shared: false
tags:
- name: api.model
Flush all caches after adding or removing a source, because the list of
sources is compiled into the dependency injection container:
vendor/.
The mapping
Each key of
MAPPING
is a column of
tx_. Each value is an array with these
keys:
api(required)Path - XPath of the value, relative to the job element, for example
title/.de is(required)Date trueconverts the value to a Unix timestamp. Supported formats areY-,m- d H: i: s O Y-andm- d H: i: s Y-. An empty value orm- d 0results in0.default(required)- Used if the XML element is missing, or if the value cannot be converted, for example a date in an unknown format. The default is stored as it is, without date conversion and without prefix. An XML element that exists but is empty stays empty.
prefix(optional)- Stored as
<prefix>_<value>. Forvacancy_, the prefix must be theid NAMEof the source, because the import finds existing jobs by<NAME>_<vacancy ID>and recognizes the jobs of a source by this prefix.
These columns are set by the import itself and need no mapping:
pid
(the storage page of the command),
is_ and
address
.
job_ and
job_ are read from the
default paths below; map them only to read their titles from another
element. Their mapping is never written as it is, its titles are always
resolved to records.
The import sets no employer address. Name and logo of the employer are taken from the address, which editors select in the field Address of the employer (company and first image of the address). The structured data of a job without employer address uses the company of the workplace address as organization name.
Expected XML structure
The services of the import expect some elements at fixed paths:
<?xml version="1.0" encoding="UTF-8"?>
<jobs>
<job>
<vacancy_id>4711</vacancy_id>
<title><de>Web Developer (m/f/d)</de></title>
<description><de><p>We are looking for ...</p></de></description>
<application_email>jobs@example.org</application_email>
<publication_link>https://example.org/jobs/4711</publication_link>
<start_date>2026-12-01</start_date>
<application_deadline>2026-11-15 00:00:00 +0100</application_deadline>
<salary_mode>1</salary_mode>
<salary>
<min>4200.00</min>
<max>5600.00</max>
</salary>
<!-- Titles of occupational fields, one per line -->
<custom_select_4><de>IT
Administration</de></custom_select_4>
<!-- Title of the employment scope -->
<custom_select_2><de>Full-time</de></custom_select_2>
<manager_name>Jane Doe</manager_name>
<manager_email>jobs@example.org</manager_email>
<locations>
<location primary="true">
<technical_name>location-1</technical_name>
<de>Sample Company Ltd.</de>
<street>Example Street 1</street>
<zipcode>12345</zipcode>
<city>Sample Town</city>
</location>
</locations>
</job>
</jobs>
- Root element
- Any name. Each child element is one job.
vacancy_id - ID of the job in the source, any string. Surrounding whitespace is removed. Required to update existing jobs instead of creating new ones; jobs without vacancy ID are skipped.
custom_select_ 4/ de - Titles of the occupational fields, one per line.
custom_select_ 2/ de - Title of the employment scope.
locations/location [@primary="true"] - The primary location, which becomes the workplace address. Its
technical_is stored as import key of the address,name deas company, plusstreet,zipcodeandcity. A job without primary location is skipped. manager_,name manager_email - Name and email of the address.
Occupational fields and employment scopes are found by their exact title in any language, hidden records included, and the record of the default language is related. A title without a matching record is created as new record in the default language on the storage page of the command.
Jobs with invalid data are skipped with a warning in the log and do not let the command fail: jobs without vacancy ID or without primary location. Invalid email addresses and URLs are removed from the job or address with a warning, the job is imported without them. The command only fails, if a source could not be fetched or a job could not be saved.
Salary information of imported jobs
Like all jobs, imported jobs are only shown with salary information, as
long as the site setting jobboard.salary.required is enabled
(the default). Map
salary_ to an XML element with the value 1 (free salary
entry) and map
salary_ and
salary_. Required fields of
the backend form, like the reference number or the start date, are not
checked by the import.