Connect a job source to the import 

The job import reads jobs from XML sources. Each source is a PHP class that implements \JWeiland\Jobboard\ApiModel\ApiModelInterface and is tagged with api.model . The extension ships no source, add your classes to a site package.

Architecture 

Flow of the console command jobboard:import:jobs:mhm
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
Copied!

Create a source class 

EXT:my_sitepackage/Classes/Import/ExampleJobBoardApiModel.php
<?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;
}
Copied!

The trait \JWeiland\Jobboard\ApiModel\ApiModelTrait implements all methods of the interface. It needs the class constants NAME , API_ENDPOINT and MAPPING and the property private int $storagePid = 0; , 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 RequestFactory , so the HTTP settings of $GLOBALS['TYPO3_CONF_VARS']['HTTP'] (timeout, proxy) apply.
MAPPING
Maps XML values to columns of the table tx_jobboard_domain_model_job , see The mapping.

Register the class 

With the attribute #[Autoconfigure(tags: ['api.model'], shared: false)] , 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.yaml:

EXT:my_sitepackage/Configuration/Services.yaml
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
Copied!

Flush all caches after adding or removing a source, because the list of sources is compiled into the dependency injection container: vendor/bin/typo3 cache:flush .

The mapping 

Each key of MAPPING is a column of tx_jobboard_domain_model_job . Each value is an array with these keys:

apiPath (required)
XPath of the value, relative to the job element, for example title/de.
isDate (required)
true converts the value to a Unix timestamp. Supported formats are Y-m-d H:i:s O, Y-m-d H:i:s and Y-m-d. An empty value or 0 results in 0 .
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>. For vacancy_id , the prefix must be the NAME of 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_import , address , job_areas and job_type .

Expected XML structure 

The services of the import expect some elements at fixed paths:

Example XML of a source
<?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>&lt;p&gt;We are looking for ...&lt;/p&gt;</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>
Copied!
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_name is stored as import key of the address, de as company, plus street, zipcode and city. 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_mode to an XML element with the value 1 (free salary entry) and map salary_min and salary_max . Required fields of the backend form, like the reference number or the start date, are not checked by the import.