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()
├── for each service tagged "api.model"
│ ├── XmlClient: GET request to getApiEndpoint()
│ │ (failed request or invalid XML: source skipped,
│ │ all its jobs are kept)
│ ├── every child element of the XML root element = one job
│ │ (jobs without vacancy_id are skipped)
│ ├── TtAddressService: address of the primary location
│ ├── JobService: job record built from getMapping()
│ └── DataHandler: save address and job
└── DataHandler: delete imported jobs that a successfully
fetched source no longer delivers
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' => '',
],
'employer' => [
'apiPath' => 'company/de',
'isDate' => false,
'default' => '',
],
'email' => [
'apiPath' => 'application_email',
'isDate' => false,
'default' => '',
],
'link' => [
'apiPath' => 'publication_link',
'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_,
address
,
job_ and
job_.
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>
<company><de>Sample Company Ltd.</de></company>
<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 the table of the job board. Titles without a matching record are skipped, records are not created.
Salary information of imported jobs
Imported jobs are only shown with salary information, like all jobs. 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.