JPKCom ACF Jobs

Documentation

Table of Contents

Packages

JPKCom
JPKComAcfJobs
JPKComAcfJobsGitUpdate

Namespaces

JPKComAcfJobsGitUpdate

Constants

JPKCOM_ACFJOBS_ABILITIES  : mixed = \true
JPKCOM_ACFJOBS_ABILITY_CATEGORY  : mixed = 'jpkcom-content'
Ability category slug, shared with the other JPKCom content plugins.
JPKCOM_ACFJOBS_ABILITY_COUNT_LIMIT  : mixed = 500
Number of listed jobs above which list-filters stops tallying.
JPKCOM_ACFJOBS_ABILITY_INPUT_KEYS  : mixed = ['jpkcom-acf-jobs/list-filters' => [], 'jpkcom-...
Top-level input keys each ability declares.
JPKCOM_ACFJOBS_ABILITY_JOB_TYPE_FIELD  : mixed = 'field_68de7a25cd78d'
Field key of the job_type checkbox, the source of the filter vocabulary.
JPKCOM_ACFJOBS_ABILITY_MAX_VALUES  : mixed = 20
Maximum number of values accepted per filter axis.
JPKCOM_ACFJOBS_ABILITY_PAGE_MAX  : mixed = \intdiv(\PHP_INT_MAX, \max(1, \JPKCOM_ACFJOBS_A...
Highest page number the query ability will ask the database for.
JPKCOM_ACFJOBS_ABILITY_PER_PAGE_DEFAULT  : mixed = 10
Default page size of the query ability.
JPKCOM_ACFJOBS_ABILITY_PER_PAGE_MAX  : mixed = 50
Maximum page size of the query ability.
JPKCOM_ACFJOBS_ABILITY_SEARCH_MAX_BYTES  : mixed = 1600
Longest search term, in bytes, that WordPress will actually apply.
JPKCOM_ACFJOBS_ABILITY_VOCABULARY_LIMIT  : mixed = 500
Maximum number of company or location records list-filters offers.
JPKCOM_ACFJOBS_BASENAME  : mixed = \plugin_basename(__FILE__)
JPKCOM_ACFJOBS_PLUGIN_PATH  : mixed = \plugin_dir_path(__FILE__)
JPKCOM_ACFJOBS_PLUGIN_URL  : mixed = \plugin_dir_url(__FILE__)
JPKCOM_ACFJOBS_VERSION  : mixed = '1.5.5'

Functions

jpkcom_acf_jobs_abilities_enabled()  : bool
Decide whether the abilities may be registered at all.
jpkcom_acf_jobs_ability_log()  : void
Record an abilities failure that WordPress itself reports silently.
jpkcom_acf_jobs_ability_json_object()  : array<string|int, mixed>|stdClass
Force a map to encode as a JSON object rather than as an array.
jpkcom_acf_jobs_ability_error()  : WP_Error
Build a WP_Error that carries an HTTP status.
jpkcom_acf_jobs_ability_capability()  : string
Resolve the capability required to run an ability.
jpkcom_acf_jobs_ability_meta()  : array<string|int, mixed>
Build the meta array of an ability.
jpkcom_acf_jobs_job_type_choices()  : array<string|int, mixed>
Read the registered job_type vocabulary as value => label.
jpkcom_acf_jobs_ability_language()  : string
Resolve the language the answer is actually in.
jpkcom_acf_jobs_ability_related_vocabulary()  : array<string|int, mixed>
List every published company or location this site offers as a filter value.
jpkcom_acf_jobs_ability_count_query()  : int
Count the posts matching a set of WP_Query arguments without fetching them.
jpkcom_acf_jobs_ability_visibility_counts()  : array<string|int, mixed>
Count the published jobs the site visibility rule excludes, by cause.
jpkcom_acf_jobs_ability_validate_input_keys()  : true|WP_Error
Refuse a top-level input key the ability does not declare.
jpkcom_acf_jobs_ability_normalise_bool()  : mixed
Read a boolean the way a GET query string can actually express one.
jpkcom_acf_jobs_ability_clamp_per_page()  : int
Clamp a requested page size into the range the query ability allows.
jpkcom_acf_jobs_ability_normalise_filter()  : array<string|int, mixed>|WP_Error
Validate one filter axis, refusing anything that would normalise away.
jpkcom_acf_jobs_ability_has_meta_clause()  : bool
Whether a meta query carries any clause on a meta key.
jpkcom_acf_jobs_ability_unbacked_claim()  : string
Report the first filter the response would claim that the query does not carry.
jpkcom_acf_jobs_ability_canonical()  : string
Reduce any query fragment to one string that stands for its exact content.
jpkcom_acf_jobs_ability_group_label()  : string
Name a query fragment for an error message a site owner has to act on.
jpkcom_acf_jobs_ability_query_commitments()  : array<string|int, mixed>
Record the query the ability is about to hand to the site, clause by clause.
jpkcom_acf_jobs_ability_query_divergence()  : string
Report the first commitment the executed query does not keep.
jpkcom_acf_jobs_ability_group_relation()  : string
Read the relation of a meta_query or tax_query group.
jpkcom_acf_jobs_ability_permission_list_filters()  : bool
Permission callback for jpkcom-acf-jobs/list-filters.
jpkcom_acf_jobs_ability_permission_query_jobs()  : bool
Permission callback for jpkcom-acf-jobs/query-jobs.
jpkcom_acf_jobs_ability_permission_get_job()  : bool
Permission callback for jpkcom-acf-jobs/get-job.
jpkcom_acf_jobs_get_ability_definitions()  : array<string|int, mixed>
Build the registration arguments for every ability this plugin provides.
jpkcom_acf_jobs_ability_list_filters_inner()  : array<string|int, mixed>|WP_Error
Execute callback for jpkcom-acf-jobs/list-filters.
jpkcom_acf_jobs_ability_query_jobs_inner()  : array<string|int, mixed>|WP_Error
Execute callback for jpkcom-acf-jobs/query-jobs.
jpkcom_acf_jobs_ability_job_is_listed()  : bool
Ask the site visibility rule whether it returns this one job.
jpkcom_acf_jobs_detail_page_renders()  : bool
Decide whether a job's own page would render for an anonymous visitor.
jpkcom_acf_jobs_ability_get_job_inner()  : array<string|int, mixed>|WP_Error
Execute callback for jpkcom-acf-jobs/get-job.
jpkcom_acf_jobs_register_ability_category()  : void
Register the shared JPKCom content ability category.
jpkcom_acf_jobs_register_abilities()  : void
Register every ability this plugin provides.
jpkcom_acf_jobs_ability_boundary()  : array<string, mixed>|WP_Error
Run an ability body and convert any Throwable into a WP_Error
jpkcom_acf_jobs_ability_list_filters()  : array<string, mixed>|WP_Error
list-filters, behind the callback boundary
jpkcom_acf_jobs_ability_query_jobs()  : array<string, mixed>|WP_Error
query-jobs, behind the callback boundary
jpkcom_acf_jobs_ability_get_job()  : array<string, mixed>|WP_Error
get-job, behind the callback boundary
jpkcom_acf_jobs_disable_archive_field()  : void
Render disable archive checkbox field
jpkcom_acf_jobs_redirect_url_field()  : void
Render archive redirect URL field
jpkcom_acf_jobs_options_page()  : void
Render Options admin page
jpkcom_acf_jobs_breadcrumb()  : void
Output Bootstrap 5 breadcrumb navigation
jpkcom_render_acf_fields()  : void
Renders all ACF fields of a post with Bootstrap 5 markup and smart icons
acf_get_field_label()  : string
Get ACF field label by field key or field name
jpkcom_get_acf_field_label()  : string
Get ACF field label with enhanced search capabilities
jpkcom_human_readable_relative_date()  : string
Convert timestamp to human-readable relative date string
jpkcom_acf_jobs_build_job_query_args()  : array<string|int, mixed>
Build WP_Query arguments for the job visibility rule.
jpkcom_acf_jobs_normalise_choices()  : array<string|int, mixed>
Normalise an ACF choice-list value to {value,label} pairs.
jpkcom_acf_jobs_normalise_choice()  : array<string|int, mixed>|null
Normalise a single-choice ACF value (button_group, select) to one pair.
jpkcom_acf_jobs_normalise_date()  : string|null
Normalise a stored date to Y-m-d.
jpkcom_acf_jobs_plain_text()  : string
Reduce stored markup to plain text without executing anything.
jpkcom_acf_jobs_normalise_related()  : array<string|int, mixed>
Project ACF post-object values to {id,title}, dropping anything unpublished.
jpkcom_acf_jobs_attachment_url()  : string|null
Resolve an ACF image value to a single URL.
jpkcom_acf_jobs_get_job_data()  : array<string|int, mixed>
Read one job as a JSON-serialisable array.
jpkcom_acf_jobs_media_size()  : void
Register custom image sizes for job posts
jpkcom_acf_jobs_image_sizes_to_selector()  : array<string|int, string>
Add custom image sizes to media library size selector
jpkcom_acf_jobs_pagination()  : void
Output Bootstrap 5 pagination navigation
jpkcom_acf_jobs_get_schema_job_posting()  : string
Generate Schema.org JobPosting JSON-LD for a single job post
jpkcom_acf_jobs_locate_template()  : string|false
jpkcom_acf_jobs_locate_template()  : string|false
Locate template file with override support
jpkcom_acf_jobs_template_include()  : string
Template loader for singular and archive templates
jpkcom_acf_jobs_get_template_part()  : void
Load partial templates with full override support
jpkcom_acfjobs_textdomain()  : void
Load plugin text domain for translations
jpkcom_acfjobs_locate_file()  : string|null
Locate file with override support

Constants

JPKCOM_ACFJOBS_ABILITIES

public mixed JPKCOM_ACFJOBS_ABILITIES = \true

JPKCOM_ACFJOBS_ABILITY_CATEGORY

Ability category slug, shared with the other JPKCom content plugins.

public mixed JPKCOM_ACFJOBS_ABILITY_CATEGORY = 'jpkcom-content'
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_COUNT_LIMIT

Number of listed jobs above which list-filters stops tallying.

public mixed JPKCOM_ACFJOBS_ABILITY_COUNT_LIMIT = 500
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_INPUT_KEYS

Top-level input keys each ability declares.

public mixed JPKCOM_ACFJOBS_ABILITY_INPUT_KEYS = ['jpkcom-acf-jobs/list-filters' => [], 'jpkcom-acf-jobs/query-jobs' => ['job_type', 'company', 'location', 'attribute', 'search', 'include_closed', 'page', 'per_page', 'order'], 'jpkcom-acf-jobs/get-job' => ['id']]

One list, in one place, because the call sites used to carry it inline and one of the three did not carry it at all. get-job accepted any undeclared key with a 200 while query-jobs and list-filters refused the same key with a 400 — measured over the REST route on WordPress 7.0.3. That is the trap the comment at the first call site already named: a caller that learns the refusal on one ability assumes it everywhere, and the one ability that silently accepts is the one it will trust.

On get-job an ignored key cannot widen a result set the way it can on query-jobs — the answer is determined by id alone — so this is a consistency defect rather than a wrong answer. It is still the shape that teaches a caller the wrong rule.

Keep in step with the input schemas: tests/test-abilities.php compares this map against the properties of every registered schema and fails the build on either drift direction. Declaring additionalProperties => false would derive the list automatically, but it was measured to preempt jpkcom_acf_jobs_ability_validate_input_keys() entirely — validate_input() runs before the execute callback — and core's replacement message names neither the accepted keys nor where a nested filter belongs.

Tags
since
1.5.0

JPKCOM_ACFJOBS_ABILITY_JOB_TYPE_FIELD

Field key of the job_type checkbox, the source of the filter vocabulary.

public mixed JPKCOM_ACFJOBS_ABILITY_JOB_TYPE_FIELD = 'field_68de7a25cd78d'
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_MAX_VALUES

Maximum number of values accepted per filter axis.

public mixed JPKCOM_ACFJOBS_ABILITY_MAX_VALUES = 20
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_PAGE_MAX

Highest page number the query ability will ask the database for.

public mixed JPKCOM_ACFJOBS_ABILITY_PAGE_MAX = \intdiv(\PHP_INT_MAX, \max(1, \JPKCOM_ACFJOBS_ABILITY_PER_PAGE_MAX))

Derived, not picked. WP_Query::get_posts() computes its LIMIT offset as absint( ( $page - 1 ) * $posts_per_page ), and that product is a plain PHP integer multiplication: past PHP_INT_MAX it becomes a float, and absint() casts rather than throws. Measured on WP 7.0.2 — page 1844674407370955161 at a page size of 10 collapses the offset to 0, so page ONE's records come back labelled as a page far beyond total_pages and a caller paginating on those numbers is handed the same records twice.

per_page is clamped to at most PER_PAGE_MAX before this bound is applied, so bounding the page at intdiv( PHP_INT_MAX, PER_PAGE_MAX ) keeps the product exact for every page size the ability accepts. max() guards the divisor: a site is free to redefine PER_PAGE_MAX, and intdiv() by zero is a fatal at file load.

Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_PER_PAGE_DEFAULT

Default page size of the query ability.

public mixed JPKCOM_ACFJOBS_ABILITY_PER_PAGE_DEFAULT = 10
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_PER_PAGE_MAX

Maximum page size of the query ability.

public mixed JPKCOM_ACFJOBS_ABILITY_PER_PAGE_MAX = 50
Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_SEARCH_MAX_BYTES

Longest search term, in bytes, that WordPress will actually apply.

public mixed JPKCOM_ACFJOBS_ABILITY_SEARCH_MAX_BYTES = 1600

Read out of wp-includes/class-wp-query.php:866-869 rather than guessed. The guard there is an anti-DoS measure that silently empties s when it is not scalar or longer than 1600 bytes, and it runs INSIDE WP_Query — after any caller has finished inspecting the arguments it passed. The result is not a crash: the search simply stops narrowing and every post matches.

Verified byte-identical on WordPress 6.9.4 (the declared floor) and 7.0.2, so there is no stricter of the two to take.

strlen(), so the unit is BYTES. Counting characters would hand a 900- character accented term to a guard that counts bytes and reproduce the defect for non-ASCII callers only.

Tags
since
1.4.0

JPKCOM_ACFJOBS_ABILITY_VOCABULARY_LIMIT

Maximum number of company or location records list-filters offers.

public mixed JPKCOM_ACFJOBS_ABILITY_VOCABULARY_LIMIT = 500
Tags
since
1.4.0

JPKCOM_ACFJOBS_BASENAME

public mixed JPKCOM_ACFJOBS_BASENAME = \plugin_basename(__FILE__)

JPKCOM_ACFJOBS_PLUGIN_PATH

public mixed JPKCOM_ACFJOBS_PLUGIN_PATH = \plugin_dir_path(__FILE__)

JPKCOM_ACFJOBS_PLUGIN_URL

public mixed JPKCOM_ACFJOBS_PLUGIN_URL = \plugin_dir_url(__FILE__)

JPKCOM_ACFJOBS_VERSION

public mixed JPKCOM_ACFJOBS_VERSION = '1.5.5'

Functions

jpkcom_acf_jobs_abilities_enabled()

Decide whether the abilities may be registered at all.

jpkcom_acf_jobs_abilities_enabled() : bool

ACF Pro is part of the condition, not an assumption. Requires Plugins only blocks activation: WordPress does not stop an administrator from deactivating a dependency that has active dependents, so one click is enough to remove get_field() from under this file. Without that check the first field read is Call to undefined function — an uncaught fatal in a REST request on the 6.9 floor.

Tags
since
1.4.0
Return values
bool

True when the Abilities API is present, ACF is active and the kill switch is on.

jpkcom_acf_jobs_ability_log()

Record an abilities failure that WordPress itself reports silently.

jpkcom_acf_jobs_ability_log(string $message) : void

wp_register_ability() and wp_register_ability_category() return null on every failure path and report only through _doing_it_wrong(), which is silent in production. Debug-only on purpose: this exists to make a failed registration findable, not to fill a production log.

Parameters
$message : string

Message to record.

Tags
since
1.4.0

jpkcom_acf_jobs_ability_json_object()

Force a map to encode as a JSON object rather than as an array.

jpkcom_acf_jobs_ability_json_object(array<string|int, mixed> $map) : array<string|int, mixed>|stdClass

PHP serialises an empty array as [], but filters, unknown and every top-level input default are declared type: object. Core's REST list controller special-cases exactly that value and rewrites it to }; the MCP Adapter does not — it hands the schema to clients raw. So this wrapper is required, not decorative.

Parameters
$map : array<string|int, mixed>

Map that must not degrade to a JSON array.

Tags
since
1.4.0
Return values
array<string|int, mixed>|stdClass

The map, or an empty object when it is empty.

jpkcom_acf_jobs_ability_error()

Build a WP_Error that carries an HTTP status.

jpkcom_acf_jobs_ability_error(string $code, string $message[, int $status = 400 ]) : WP_Error

The status is never optional in practice. The REST run controller returns the WP_Error verbatim and rest_ensure_response() defaults to 500 when data['status'] is absent — which tells an agent "transient server fault, retry unchanged", the exact opposite of the intended instruction. These messages exist so a caller can correct itself in one turn.

Parameters
$code : string

Machine-readable error code.

$message : string

Human-readable message naming the valid form.

$status : int = 400

HTTP status to report. Default 400.

Tags
since
1.4.0
Return values
WP_Error

Error carrying the status in its data.

jpkcom_acf_jobs_ability_capability()

Resolve the capability required to run an ability.

jpkcom_acf_jobs_ability_capability(string $ability) : string

Defaults to read. Every query is hard-scoped to published, unprotected jobs, so this cannot expose drafts or private content — but it is bulk machine-readable access, which a site may want to restrict further.

Parameters
$ability : string

Fully qualified ability name.

Tags
since
1.4.0
Return values
string

Capability name.

jpkcom_acf_jobs_ability_meta()

Build the meta array of an ability.

jpkcom_acf_jobs_ability_meta(string $ability) : array<string|int, mixed>

Three independent exposure switches live here. show_in_rest governs core REST visibility. public seeds it from WordPress 7.1 onwards and is an inert passthrough on 6.9 and 7.0. mcp is not a core key at all — it is the MCP Adapter's own gate, and without it an ability is neither discoverable nor executable over MCP.

All three annotations are set explicitly. They default to null, and the REST run controller derives the HTTP method from them, so an ability without annotations is POST-only.

Parameters
$ability : string

Fully qualified ability name.

Tags
since
1.4.0
Return values
array<string|int, mixed>

Meta array for wp_register_ability().

jpkcom_acf_jobs_job_type_choices()

Read the registered job_type vocabulary as value => label.

jpkcom_acf_jobs_job_type_choices() : array<string|int, mixed>

Read from the field definition, never from stored values, so the enum is complete even for a type no job currently uses. The eight literals are a fallback rather than a second source of truth: they keep jpkcom_acf_jobs_get_ability_definitions() free of any dependency on a live ACF registry, which is what lets the CI harness assert the registration arrays without a WordPress installation.

The labels are locale-dependent and are never valid filter input. Only the keys are.

Tags
since
1.4.0
Return values
array<string|int, mixed>

Map of job type value => human-readable label.

jpkcom_acf_jobs_ability_language()

Resolve the language the answer is actually in.

jpkcom_acf_jobs_ability_language() : string

There is deliberately no lang input anywhere in this feature. Nothing here can switch WPML's language context, and a declared parameter with nothing behind it is a false statement in the schema: a client sending lang=fr would receive German and have no way to notice. Reporting what the site resolved is the honest half of that trade.

Both WPML accessors are guarded. wpml_current_language is the documented filter and returns the value it was handed when WPML is absent, so no plugin check is needed around it; ICL_LANGUAGE_CODE is read only after defined(), because referencing an undefined constant is a fatal on PHP 8.

Tags
since
1.4.0
Return values
string

Language or locale code, empty only when WordPress itself cannot say.

List every published company or location this site offers as a filter value.

jpkcom_acf_jobs_ability_related_vocabulary(string $post_type) : array<string|int, mixed>

Deliberately independent of how many jobs exist. Deriving this list from the pass over the visible jobs would make its contents depend on the corpus size, so a caller would be offered a different filter menu on a large site than on a small one with nothing in the response explaining why. Only the counts depend on that pass; the vocabulary does not.

fields => 'ids' so the query itself carries no post rows, no_found_rows because nothing here needs a total, and both cache flags off because the caller primes exactly the meta it goes on to read. The titles and the second status check then come from jpkcom_acf_jobs_normalise_related(), which is the same projection every other reader in this feature uses and which never emits a WP_Post. A direct SELECT of ID and post_title would touch fewer columns, but this plugin issues no SQL of its own anywhere, and it would bypass that status and password recheck.

Parameters
$post_type : string

Either 'job_company' or 'job_location'.

since
1.4.0

One record more than the cap is fetched so that hitting the cap is detectable at all. Asking for exactly the cap alongside no_found_rows would make the truncation invisible to this function itself: a site with 600 companies would return 500 and have no way to know it had dropped 100.

Return values
array<string|int, mixed>

{ @type array $records List of [ 'id' => int, 'title' => string ]. @type bool $truncated Whether the site holds more records than the cap. }

jpkcom_acf_jobs_ability_count_query()

Count the posts matching a set of WP_Query arguments without fetching them.

jpkcom_acf_jobs_ability_count_query(array<string|int, mixed> $args) : int

fields => 'ids' plus a page size of one keeps the result to a single column of a single row while the total is still computed. no_found_rows is pinned to false because that total is the only thing this call exists to produce.

Parameters
$args : array<string|int, mixed>

WP_Query arguments.

Tags
since
1.4.0
Return values
int

Number of matching posts.

jpkcom_acf_jobs_ability_visibility_counts()

Count the published jobs the site visibility rule excludes, by cause.

jpkcom_acf_jobs_ability_visibility_counts(int $published_total, int $listed_total) : array<string|int, mixed>

Two independent causes exclude a job that has no job_featured row: the EXISTS clause of the visibility rule, and the meta_key the ordering needs, whose postmeta.meta_key condition lands in the WHERE clause. Removing either one changes nothing, so subtracting a listed total from a published total would attribute the shortfall to whichever cause happened to be named. Each cause therefore gets its own query.

The expired count is conditioned on job_featured existing as well, so the two causes partition the difference rather than overlapping.

Parameters
$published_total : int
$listed_total : int
Tags
since
1.4.0
Return values
array<string|int, mixed>

{ @type int $hidden_missing_featured Published jobs carrying no job_featured row. @type int $hidden_expired Published jobs whose expiry date has passed. }

jpkcom_acf_jobs_ability_validate_input_keys()

Refuse a top-level input key the ability does not declare.

jpkcom_acf_jobs_ability_validate_input_keys(array<string, mixed> $input, array<string|int, string> $allowed) : true|WP_Error

Neither input schema declares additionalProperties, so an unrecognised key reaches the callback and is simply not read. The observable result was the complete unfiltered corpus behind an HTTP 200, with filters correctly omitting what it had not applied and unknown empty - so the caller had to notice an absence to notice the failure, and one that trusts total reports the whole corpus as a filtered answer.

Two routes led there and both are common. The output schema of query-jobs instructed the model to send a work_type filter that has never existed as an input, and a single transposed letter in a real axis behaved identically.

Parameters
$input : array<string, mixed>

Raw ability input.

$allowed : array<string|int, string>

Declared input keys.

Tags
since
1.4.0
Return values
true|WP_Error

True when every key is declared, WP_Error otherwise.

jpkcom_acf_jobs_ability_normalise_bool()

Read a boolean the way a GET query string can actually express one.

jpkcom_acf_jobs_ability_normalise_bool(mixed $value) : mixed

Mirrors core's rest_sanitize_boolean() rather than inventing a set: those are the spellings every other WordPress REST endpoint accepts, so a caller that has learned one surface has learned this one. Anything outside them comes back unchanged, so the caller still gets the 400 - widening what is accepted must not turn into guessing what was meant.

Parameters
$value : mixed

Raw input value.

Tags
since
1.4.0
Return values
mixed

A bool when the value has a boolean reading, the input otherwise.

jpkcom_acf_jobs_ability_clamp_per_page()

Clamp a requested page size into the range the query ability allows.

jpkcom_acf_jobs_ability_clamp_per_page(mixed $value) : int

Never returns -1 or 0. The shortcode's own default IS -1 and the shared builder refuses to default to it, but this clamp is what keeps an API caller from reaching an unbounded query in the first place.

Clamping rather than refusing is deliberate here, and it is not the same decision as the one the filter axes take. The response echoes the applied page size back, so a caller can see what it got; a silently dropped filter clause has no such tell, which is why that case is an error instead.

Parameters
$value : mixed

Requested page size.

Tags
since
1.4.0
Return values
int

Page size between 1 and JPKCOM_ACFJOBS_ABILITY_PER_PAGE_MAX.

jpkcom_acf_jobs_ability_normalise_filter()

Validate one filter axis, refusing anything that would normalise away.

jpkcom_acf_jobs_ability_normalise_filter(mixed $raw, string $axis, int $max) : array<string|int, mixed>|WP_Error

This is the load-bearing guard of the whole ability. The shortcode builds each clause as array_filter( array_map( 'absint', … ) ) and skips it when the result is empty, so company=["acme"] adds no clause at all and the response contains EVERY job — the same class that answered with 19 of 19 posts in jpkcom-post-filter. A requested filter that survives normalisation empty is therefore an error naming the valid form, never a dropped clause.

A well-formed value that simply matches nothing is a different statement and is not handled here: the caller resolves it and reports it in unknown, because an empty result for company=[999] is honest.

Only the shape is decided here. Whether a well-formed id or slug exists on this site is resolved at the call site, where get_post_type() and get_term_by() are available and where the unknown bucket lives.

Parameters
$raw : mixed

Raw value as it arrived in the ability input.

$axis : string

Axis name: 'job_type', 'company', 'location' or 'attribute'.

$max : int

Maximum number of values this axis accepts.

Tags
since
1.4.0
Return values
array<string|int, mixed>|WP_Error

Normalised values, [] when the axis was not requested, or an error.

jpkcom_acf_jobs_ability_has_meta_clause()

Whether a meta query carries any clause on a meta key.

jpkcom_acf_jobs_ability_has_meta_clause(mixed $meta_query, string $key[, int $depth = 0 ]) : bool

This answers one question only, and it is a question about the ability's OWN construction: did the shared builder turn the request into a clause at all. It is asked before jpkcom_acf_jobs_ability_query_args runs, because after that filter the question is no longer "is something there" but "is what is there what this ability built" — which is decided by identity, in jpkcom_acf_jobs_ability_query_divergence(), and not by inspecting properties.

Deliberately shape-tolerant. The builder is resolved through the plugin's file override chain and the spec's promise is that this ability runs the same query the site itself runs, so how a site spells its own clause is its business; that the clause exists for a filter the response is about to CLAIM is not.

The depth limit is not decoration: unbounded recursion on a deep array is a stack overflow, which no ability callback may risk.

Parameters
$meta_query : mixed

Meta query.

$key : string

Meta key to look for.

$depth : int = 0

Current recursion depth. Internal.

Tags
since
1.4.0
Return values
bool

True when at least one clause names the key.

jpkcom_acf_jobs_ability_unbacked_claim()

Report the first filter the response would claim that the query does not carry.

jpkcom_acf_jobs_ability_unbacked_claim(array<string|int, mixed> $args, array<string|int, mixed> $claims, bool $attribute, string $search, string $order) : string

A precondition on the ability's own construction, asked before any site callback can touch the arguments. The shared builder is resolved through the plugin's file override chain and skips a clause whose value list came out empty, so a request that never became a clause would be answered with every job on the site while filters names the axis as applied — the defect this whole feature exists to prevent.

It asks presence and nothing else, on purpose. What a clause has to survive between here and WP_Query is decided by identity in jpkcom_acf_jobs_ability_query_divergence(); what a site's own builder chooses to build is the site's business, and the spec's promise is that this ability runs the same query the site runs.

Nothing here casts an unvalidated value: a cast of an object without __toString is a Throwable, and a Throwable out of an ability callback is an uncaught fatal on the declared 6.9 floor.

Parameters
$args : array<string|int, mixed>

WP_Query arguments as the ability built them.

$claims : array<string|int, mixed>

Meta keys that must each carry a clause, keyed by the label to report.

$attribute : bool

Whether a job-attribute clause is required.

$search : string

Search term that must have reached the query, '' when none was requested.

$order : string

Direction both sort components must carry.

Tags
since
1.4.0
Return values
string

Label of the first unbacked claim, or '' when every claim is carried.

jpkcom_acf_jobs_ability_canonical()

Reduce any query fragment to one string that stands for its exact content.

jpkcom_acf_jobs_ability_canonical(mixed $value[, int $depth = 0 ]) : string

Two fragments produce the same string when they carry the same values under the same keys, and a different one as soon as anything about them differs — a value, an operator, a cast, an added key, an added clause, a changed type. That is the whole point: this ability cannot enumerate what a site callback might change about a clause, and it does not have to, because it knows what it built and can recognise it again.

Three properties are load-bearing:

  • Key ORDER is not significant. WP_Meta_Query reads a clause by key, so [ 'key' => …, 'compare' => … ] and [ 'compare' => …, 'key' => … ] are the same clause, and a callback that rebuilds a clause it has read has not changed it. The parts are therefore sorted before they are joined.
  • LIST order IS significant, because each part carries its own index. Two clauses swapped inside an OR group mean the same thing to core and are reported as a divergence here anyway. That direction is deliberate: this comparison may refuse a query the ability did in fact build, and may never accept one it did not.
  • Scalar TYPES are part of the value. '182' and 182 produce different SQL through a LIKE comparison, so they must not compare equal.

Strings are length-prefixed so that no punctuation inside a value can imitate the structure around it.

Nothing here throws for any input. No value is cast to string — a cast of an object without __toString is a Throwable, and a Throwable out of an ability callback is an uncaught fatal on the declared 6.9 floor — and the recursion is depth-limited. The limit is far above anything this ability builds (its deepest fragment is a clause inside a group, at depth two), so a commitment can never itself be truncated, and a filtered fragment that is deeper than the limit differs from the commitment at a shallower level in any case.

Parameters
$value : mixed

Fragment to reduce.

$depth : int = 0

Current recursion depth. Internal.

Tags
since
1.4.0
Return values
string

Canonical representation of the fragment.

jpkcom_acf_jobs_ability_group_label()

Name a query fragment for an error message a site owner has to act on.

jpkcom_acf_jobs_ability_group_label(mixed $fragment) : string

Descriptive only. Nothing about the guard depends on it: a fragment nobody anticipated is still committed and still compared, it is merely reported under a general name.

Parameters
$fragment : mixed

Query fragment.

Tags
since
1.4.0
Return values
string

Human-readable name.

jpkcom_acf_jobs_ability_query_commitments()

Record the query the ability is about to hand to the site, clause by clause.

jpkcom_acf_jobs_ability_query_commitments(array<string|int, mixed> $args) : array<string|int, mixed>

Taken immediately before jpkcom_acf_jobs_ability_query_args runs, so that what comes back can be compared against it rather than interrogated. Four review rounds asked a longer list of questions of each clause every time — presence, then compare, then type, then the enclosing relation — and every round found a question the previous one had not thought to ask. A value was still not among them, and neither was a clause added inside an OR group. The list of things that can be altered about a clause is not bounded by what anyone thought of; the set of clauses this ability built is.

Committed: every top-level element of meta_query and of tax_query. Those two structures are the whole of what a callback may contribute, so they are the whole of what can come back changed — every other query var is simply never read from the filtered array, which is a stronger guarantee than any comparison and needs no list of names to hold.

Parameters
$args : array<string|int, mixed>

WP_Query arguments as the ability built them.

Tags
since
1.4.0
Return values
array<string|int, mixed>

{ @type array $meta List of [ 'label' => string, 'canonical' => string ] for meta_query. @type array $tax The same for tax_query. }

jpkcom_acf_jobs_ability_query_divergence()

Report the first commitment the executed query does not keep.

jpkcom_acf_jobs_ability_query_divergence(array<string|int, mixed> $committed, mixed $args) : string

The rule has two halves and both are established here rather than assumed.

Every clause this ability built has to be present, byte for byte as it was built, as a direct element of the same group. Anything else about it — a value trimmed, a boundary moved, a term list widened, one more value added inside an OR group, an operator flipped, a key nobody has thought of yet — makes it a different clause, and a different clause is not the one the response is about to name as applied.

What a callback may still do is ADD, and that permission rests on the relation of the group it adds to: under AND every further element can only remove rows, which is the one direction that cannot turn the response into a false claim. So the relation is verified on the arguments that actually run, and an addition anywhere else is refused by the paragraph above — an extra value inside an axis OR group widened FULL_TIME from 2 jobs to 4 on /home/jpk/ddev/posts while the response still claimed FULL_TIME.

A commitment is consumed once it is matched, so two committed clauses need two elements to satisfy them.

Nothing here throws for any input: every comparison is between two strings produced by jpkcom_acf_jobs_ability_canonical().

Parameters
$committed : array<string|int, mixed>

Output of jpkcom_acf_jobs_ability_query_commitments().

$args : mixed

WP_Query arguments as they will reach WP_Query.

Tags
since
1.4.0

Only the two contributable structures are compared, because they are the only ones a callback can reach. Everything else the query carries is the array this ability built, taken from nowhere else.

Return values
string

Name of the first divergence, or '' when the query is the one that was built.

jpkcom_acf_jobs_ability_group_relation()

Read the relation of a meta_query or tax_query group.

jpkcom_acf_jobs_ability_group_relation(mixed $group) : string

Absent means AND, which is what both WP_Meta_Query and WP_Tax_Query default to, so a missing relation is not a divergence.

Parameters
$group : mixed

Query group.

Tags
since
1.4.0
Return values
string

'AND' or the uppercased relation as given.

jpkcom_acf_jobs_ability_permission_list_filters()

Permission callback for jpkcom-acf-jobs/list-filters.

jpkcom_acf_jobs_ability_permission_list_filters([mixed $input = null ]) : bool
Parameters
$input : mixed = null

Validated ability input. Unused.

Tags
since
1.4.0
Return values
bool

True when the current user may run the ability.

jpkcom_acf_jobs_ability_permission_query_jobs()

Permission callback for jpkcom-acf-jobs/query-jobs.

jpkcom_acf_jobs_ability_permission_query_jobs([mixed $input = null ]) : bool
Parameters
$input : mixed = null

Validated ability input. Unused.

Tags
since
1.4.0
Return values
bool

True when the current user may run the ability.

jpkcom_acf_jobs_ability_permission_get_job()

Permission callback for jpkcom-acf-jobs/get-job.

jpkcom_acf_jobs_ability_permission_get_job([mixed $input = null ]) : bool
Parameters
$input : mixed = null

Validated ability input. Unused.

Tags
since
1.4.0
Return values
bool

True when the current user may run the ability.

jpkcom_acf_jobs_get_ability_definitions()

Build the registration arguments for every ability this plugin provides.

jpkcom_acf_jobs_get_ability_definitions() : array<string|int, mixed>

Touches no registry and reads no WordPress state beyond __() and the jpkcom_acf_jobs_ability_meta filter, which is what lets the CI harness assert these arrays without a WordPress installation. Not free of side effects, though: __() and the three meta calls each fire apply_filters(), so third-party callbacks run whenever this is called.

Every output property is optional. A theme may replace includes/acf-field_groups.php through the plugin's file override system, after which ACF falls back to the raw meta and several values arrive in a different shape.

Tags
since
1.4.0
Return values
array<string|int, mixed>

Ability name => wp_register_ability() arguments.

jpkcom_acf_jobs_ability_list_filters_inner()

Execute callback for jpkcom-acf-jobs/list-filters.

jpkcom_acf_jobs_ability_list_filters_inner([mixed $input = null ]) : array<string|int, mixed>|WP_Error

Answers "which values does query-jobs accept, and how much of this site do they actually reach". The job type vocabulary comes from the field definition rather than from stored values, so it is complete even for a type nobody uses today; the attributes come from the term relations, because load_terms makes ACF discard the stored meta and a slug read from that meta may name a term the job no longer carries.

The counts and the visibility summary come from one pass over the listed job IDs rather than from one query per value.

Parameters
$input : mixed = null

Validated ability input. This ability takes none.

Tags
since
1.4.0
Return values
array<string|int, mixed>|WP_Error

The filter vocabulary, or an error.

jpkcom_acf_jobs_ability_query_jobs_inner()

Execute callback for jpkcom-acf-jobs/query-jobs.

jpkcom_acf_jobs_ability_query_jobs_inner([mixed $input = null ]) : array<string|int, mixed>|WP_Error

Answers "which jobs does this site list, narrowed by these filters" with the same query the site itself runs, plus three things the site never needed:

  1. A requested filter that normalises to nothing is refused with a 400. The shared builder skips a clause whose value list is empty, so company= ["acme"] would otherwise return every job as a filtered answer. Two further guards stand behind that one, and they answer different questions. Before the argument filter runs, every filter the response is about to CLAIM has to have become a clause — the builder is overridable and a request that never became a clause cannot go missing later. After it runs, the query that will execute has to be the query this ability built: each clause is recorded beforehand and compared by content, so a value, a boundary, a term list, an operator or a key nobody has thought of yet all read the same way — this is not the query that was built, and no answer is given for it. A callback may still add, because the top-level relation of both queries is verified to be AND on the arguments that run and a further conjunct can only narrow.
  2. A deterministic tiebreaker. ORDER BY meta_value_num DESC, date DESC leaves ties unresolved and MySQL permutes tied rows per execution — measured, four fetches of unmodified code produced three orderings, because the seeded jobs share a post_date. Harmless for the site's own unpaginated listing; here an unstable sort puts a job on two pages or on none. It is appended after the builder, never inside it: the builder is shared with the shortcode and the archive, whose ordering this feature does not change. The whole sort is then committed across the argument filter — the featured-first component and the meta_key it needs, the date direction filters.order reports back, and the tiebreaker — so a site may add to the query, but not re-sort what it answers with.
  3. Real totals for a page past the last one. WP_Query::set_found_posts() returns early when posts is empty, so found_posts and max_num_pages stay 0 and a response would claim an empty corpus next to a page number of three.

Per-property defaults are resolved here. Core applies only the top-level default, and only when the input is exactly null.

Nothing in this function throws for any input, and nothing casts an unvalidated value: the shared builder casts order to string, which throws for an object without __toString, and a Throwable out of an ability callback is an uncaught fatal on the declared 6.9 floor.

Parameters
$input : mixed = null

Validated ability input.

Tags
since
1.4.0
Return values
array<string|int, mixed>|WP_Error

The result set, or an error.

jpkcom_acf_jobs_ability_job_is_listed()

Ask the site visibility rule whether it returns this one job.

jpkcom_acf_jobs_ability_job_is_listed(int $post_id) : bool

The verdict comes from the rule itself, restricted to a single post ID, rather than from a second reading of the same meta in PHP. Spec section 5.3 defines listed as whether the job satisfies the visibility rule, and the visibility rule is jpkcom_acf_jobs_build_job_query_args() — not a paraphrase of it.

A paraphrase was measured to be wrong, not merely fragile. MariaDB casts a stored job_expiry_date of '2025-11-30 00:00:00' to the DATE 2025-11-30 through the rule's own type => 'DATE' comparison and drops the job, while jpkcom_acf_jobs_normalise_date() refuses that spelling outright and every PHP reading of it concludes "not expired". get-job then reported listed: true for a job query-jobs would not return — two abilities in one feature answering the same question differently. Asking the query removes the entire class rather than that one instance of it: any later change to the rule is reflected here automatically, and no copy of it can drift again.

The jpkcom_acf_jobs_ability_query_args filter is deliberately NOT applied. It exists so a site can shape what query-jobs lists; applying it to a verdict about the site's own rule would let a callback make this answer disagree with the rule it reports on.

Parameters
$post_id : int

Job post ID.

Tags
since
1.4.0
Return values
bool

True when the site visibility rule returns this job.

jpkcom_acf_jobs_detail_page_renders()

Decide whether a job's own page would render for an anonymous visitor.

jpkcom_acf_jobs_detail_page_renders(int $post_id) : bool

This is the question the detail block depends on, and it is not the same question as "is this job listed". Address, salary, attributes and application data are public only as a side effect of a job's detail page rendering, so for a job that has no such page they were never published to anybody and emitting them to a logged-in subscriber would publish data the site has deliberately never shown. A job that is merely absent from every listing has a page like any other and keeps its detail block.

Three states answer false, and each of them is a redirect in includes/redirects.php:

  1. job_url is set — :32-86 sends every caller without manage_options to that target with a 307 before single-job.php runs.
  2. The job has expired — :132-173 sends every caller without edit_post to the job archive with a 307.
  3. The post carries a password — no public page in any meaningful sense, and the reader's own gate refuses such a job outright.

The job_url test mirrors redirects.php literally: ! empty() on the RAW value, NOT the trimmed value the reader compares. A url of " " is not empty, so the site really does redirect to it, while a reader that trims first concludes the job does not redirect at all and returns everything. That difference is the whole reason this predicate exists as a second, independent decision rather than as a comment on the reader.

The expiry test deliberately does NOT mirror redirects.php literally. redirects.php compares the stored value against a Y-m-d today as strings, and ACF hands out Ymd whenever the field's key reference row is missing — the state includes/wpml-acf-field-keys-fix.php exists to repair — so '20251130' < '2026-01-15' holds only by accident of the same leading digits. The shared normaliser is used instead, which is also what the visibility rule compares.

Nothing here throws for any input, and a false is always the safe answer: the caller may only ever narrow what it emits on the strength of it.

Parameters
$post_id : int

Job post ID.

Tags
since
1.4.0
Return values
bool

True when a visitor would be served the job's own page.

jpkcom_acf_jobs_ability_get_job_inner()

Execute callback for jpkcom-acf-jobs/get-job.

jpkcom_acf_jobs_ability_get_job_inner([mixed $input = null ]) : array<string|int, mixed>|WP_Error

Answers "everything this site publishes about job N", including the detail data only its own page carries, and answers it for jobs no listing contains — that is what separates this ability from a query-jobs of one result. A job with no job_featured row, or an expired one, is resolvable here with listed false and the reason; applying the visibility rule as an existence test instead would leave "why does job 42 appear in no list" unanswerable by any ability.

Two rules hold this callback together:

  1. One answer for "absent" and for "not readable". The same code, the same message, the same 404, and the same amount of work, for an ID that names nothing, an ID that names a draft, a page, a revision, a company or a password-protected job, and an ID that is not a positive integer. Anything else — a distinct message, an extra lookup, a log line — turns this ability into a way for every logged-in subscriber to enumerate which post IDs a site holds. Only the shape of the CALL is answered precisely: a request that names no id at all gets a 400 naming the parameter, which says nothing about any ID.
  2. The detail-page rule is re-decided here, not inherited. The reader is called with $full = true and applies the rule itself, but includes/jobs-data.php is resolved through the plugin's file override chain and a site may replace it outright. jpkcom_acf_jobs_detail_page_renders() is therefore re-run over the result. It can only ever remove the detail block, never add one, so this is a narrowing and never a disclosure.
Parameters
$input : mixed = null

Validated ability input.

Tags
since
1.4.0
Return values
array<string|int, mixed>|WP_Error

The job record, or an error.

jpkcom_acf_jobs_register_ability_category()

Register the shared JPKCom content ability category.

jpkcom_acf_jobs_register_ability_category() : void

Defensive on purpose. Categories are global and first-wins, the loser of a collision gets a silent null, and an ability registered into an unregistered category is not registered at all. jpkcom-post-filter ships the same slug, so on any site running both plugins one of the two always loses the race.

Tags
since
1.4.0

jpkcom_acf_jobs_register_abilities()

Register every ability this plugin provides.

jpkcom_acf_jobs_register_abilities() : void

wp_register_ability() returns null on every failure path and reports only through _doing_it_wrong(), which is silent in production, so each result is checked rather than assumed.

Tags
since
1.4.0

jpkcom_acf_jobs_ability_boundary()

Run an ability body and convert any Throwable into a WP_Error

jpkcom_acf_jobs_ability_boundary(callable $body, string $ability) : array<string, mixed>|WP_Error

The declared floor is WordPress 6.9, which has no Throwable-to-WP_Error wrapper - that landed in 7.0 - so an exception escaping a callback there is an uncaught fatal: a blank 500 with no body, no code and nothing a client can act on, triggerable by any logged-in subscriber. This file's own docblock already promises that every callback returns a WP_Error rather than throwing. Until this function existed, that promise covered the plugin's own arithmetic and not the reads.

It is a boundary rather than a set of shape checks on purpose. The throw happens INSIDE ACF while it reads - acf_maybe_get() for checkbox and select, acf_field_flexible_content->load_value() for layout content - so nothing on this side of the call can inspect the value first, the file's "read unformatted" rule does not reach the load path, and the set of shapes ACF cannot tolerate belongs to ACF and changes with it. An enumeration of fields or of corrupt shapes would be the same mistake the query post-condition made four times before it was replaced by one rule that needs no list.

The two paths that can degrade instead of failing do so before reaching here: query-jobs skips the unreadable job and counts it, and get-job answers as it does for an id that resolves to nothing. This catches what neither foresaw.

The message is deliberately generic. The exception text used to reach every logged-in subscriber over REST and every MCP client as an isError block, and a raw PHP engine string is neither something a caller can act on nor something a site owner wants published; it goes to the log instead.

Parameters
$body : callable

The ability body to run.

$ability : string

Ability name, for the log line.

Tags
since
1.4.0
Return values
array<string, mixed>|WP_Error

The body's result, or an error.

jpkcom_acf_jobs_ability_list_filters()

list-filters, behind the callback boundary

jpkcom_acf_jobs_ability_list_filters([mixed $input = null ]) : array<string, mixed>|WP_Error
Parameters
$input : mixed = null

Validated ability input.

Tags
since
1.4.0
Return values
array<string, mixed>|WP_Error

The vocabulary, or an error.

jpkcom_acf_jobs_ability_query_jobs()

query-jobs, behind the callback boundary

jpkcom_acf_jobs_ability_query_jobs([mixed $input = null ]) : array<string, mixed>|WP_Error
Parameters
$input : mixed = null

Validated ability input.

Tags
since
1.4.0
Return values
array<string, mixed>|WP_Error

The listing, or an error.

jpkcom_acf_jobs_ability_get_job()

get-job, behind the callback boundary

jpkcom_acf_jobs_ability_get_job([mixed $input = null ]) : array<string, mixed>|WP_Error
Parameters
$input : mixed = null

Validated ability input.

Tags
since
1.4.0
Return values
array<string, mixed>|WP_Error

The record, or an error.

jpkcom_acf_jobs_disable_archive_field()

Render disable archive checkbox field

jpkcom_acf_jobs_disable_archive_field() : void
Tags
since
1.3.0

jpkcom_acf_jobs_redirect_url_field()

Render archive redirect URL field

jpkcom_acf_jobs_redirect_url_field() : void
Tags
since
1.3.0

jpkcom_acf_jobs_options_page()

Render Options admin page

jpkcom_acf_jobs_options_page() : void
Tags
since
1.3.0

jpkcom_acf_jobs_breadcrumb()

Output Bootstrap 5 breadcrumb navigation

jpkcom_acf_jobs_breadcrumb() : void

Generates breadcrumb navigation for:

  • Single job posts (Home > Jobs > Job Title)
  • Job archive (Home > Jobs)
  • Other pages (Home > Page Title)

Includes proper ARIA labels and semantic HTML5 markup.

Tags
since
1.0.0
global

WP_Post $post Current post object.

Return values
void

Outputs HTML directly.

jpkcom_render_acf_fields()

Renders all ACF fields of a post with Bootstrap 5 markup and smart icons

jpkcom_render_acf_fields([string $post_type = '' ]) : void

Automatically detects field types and renders them with appropriate styling:

  • Images: Responsive with rounded corners
  • WYSIWYG/Textarea: Light background container
  • Relationships/Post Objects: Linked post titles
  • True/False: Badge indicators
  • Repeater: Responsive tables
  • Groups: Nested definition lists
Parameters
$post_type : string = ''

Optional. Post type for field group query. Default empty (uses current post type).

Tags
since
1.0.0
global

WP_Post $post Current post object.

acf_get_field_label()

Get ACF field label by field key or field name

acf_get_field_label(string $field_key_or_name) : string

Attempts to retrieve the field label from ACF. If not found, returns a formatted fallback based on the field name/key.

Parameters
$field_key_or_name : string

Field key (e.g., 'field_abc123') or field name (e.g., 'job_title').

Tags
since
1.0.0
Return values
string

Field label or formatted fallback string.

jpkcom_get_acf_field_label()

Get ACF field label with enhanced search capabilities

jpkcom_get_acf_field_label(string $field_name_or_key[, string $post_type = '' ]) : string

Searches for field labels in this order:

  1. Direct field key lookup (if starts with 'field_')
  2. Search through field groups by post type
  3. Search through sub_fields (repeater/group fields)
  4. Fallback to formatted field name
Parameters
$field_name_or_key : string

Field name (e.g., 'job_title') or field key (e.g., 'field_abc123').

$post_type : string = ''

Optional. Post type for context-specific field group search. Default empty.

Tags
since
1.0.0
Return values
string

Field label or formatted fallback string.

jpkcom_human_readable_relative_date()

Convert timestamp to human-readable relative date string

jpkcom_human_readable_relative_date(int $timestamp) : string

Converts Unix timestamps into relative date strings like:

  • "Published today"
  • "Published yesterday"
  • "Published 3 days ago"
  • "Published 2 weeks ago"
  • "Published 5 months ago"
  • "Published 2 years ago"

All strings are translatable via the 'jpkcom-acf-jobs' text domain.

Parameters
$timestamp : int

Unix timestamp to convert.

Tags
since
1.0.0
Return values
string

Translated relative date string.

jpkcom_acf_jobs_build_job_query_args()

Build WP_Query arguments for the job visibility rule.

jpkcom_acf_jobs_build_job_query_args([array<string|int, mixed> $args = [] ]) : array<string|int, mixed>

This is the single source for "which jobs does this site show". It is called from three places that previously each carried their own copy: the [jpkcom_acf_jobs_list] shortcode, the job archive's pre_get_posts handler, and the Abilities API callbacks.

Keys whose value is null are omitted from the result, because the archive sets its query through WP_Query::set() and must not inherit a page size or a status.

Parameters
$args : array<string|int, mixed> = []

{ Optional. Query parameters.

@type int|null    $posts_per_page             Page size. Omitted when null. Never defaulted to -1.
@type int|null    $paged                      Page number. Omitted when null.
@type string      $order                      'ASC' or 'DESC' for the date component. Default 'DESC'.
@type string|null $post_status                Post status. Omitted when null.
@type string[]    $job_type                   job_type values (not labels).
@type int[]       $company                    job_company post IDs.
@type int[]       $location                   job_location post IDs.
@type int[]       $attribute                  job-attribute term IDs.
@type string      $search                     Free-text search.
@type bool        $exclude_password_protected Whether to drop password-protected jobs.

}

Tags
since
1.4.0
Return values
array<string|int, mixed>

WP_Query arguments.

jpkcom_acf_jobs_normalise_choices()

Normalise an ACF choice-list value to {value,label} pairs.

jpkcom_acf_jobs_normalise_choices(mixed $value) : array<string|int, mixed>

A checkbox with return_format 'array' yields [ ['value'=>…, 'label'=>…], … ]. The same field yields bare strings when ACF falls back to the raw meta, which happens whenever the field group is not registered — a theme replacing acf-field_groups.php through the override system is enough. Both shapes are the contract, not a defensive afterthought.

Parameters
$value : mixed

Raw ACF value.

Tags
since
1.4.0
Return values
array<string|int, mixed>

List of [ 'value' => string, 'label' => string ].

jpkcom_acf_jobs_normalise_choice()

Normalise a single-choice ACF value (button_group, select) to one pair.

jpkcom_acf_jobs_normalise_choice(mixed $value) : array<string|int, mixed>|null
Parameters
$value : mixed

Raw ACF value.

Tags
since
1.4.0
Return values
array<string|int, mixed>|null

[ 'value' => string, 'label' => string ], or null when empty.

jpkcom_acf_jobs_normalise_date()

Normalise a stored date to Y-m-d.

jpkcom_acf_jobs_normalise_date(mixed $raw) : string|null

ACF stores date fields as Ymd and formats them on read, so both spellings reach this function depending on whether the field's key reference row exists. Anything else yields null rather than a guess.

Deliberately not written as date( 'Y-m-d', strtotime( $raw ) ), the form used in includes/schema.php:71: under strict_types a false from strtotime() makes date() throw a TypeError, and on the WP 6.9 floor a Throwable out of an ability callback is an uncaught fatal.

is_string() is not enough to make createFromFormat() safe: a PHP string can carry an embedded NUL byte anywhere in it, and as of PHP 8.3 that makes DateTimeImmutable::createFromFormat() throw ValueError instead of returning false — confirmed regardless of the NUL's position or which of the two formats below is tried. MySQL longtext happily stores one; an importer, WP-CLI, a WPML copy or direct SQL against job_expiry_date is enough to plant it. The try/catch below is what closes that door; everything else that can go wrong here (invalid UTF-8, absurdly long input, an overflowing month/day/date) was measured to fail closed already — createFromFormat() returns false rather than throwing, and format() never throws for the two hardcoded, always-valid format strings used here.

Parameters
$raw : mixed

Stored value.

Tags
since
1.4.0
Return values
string|null

Date as Y-m-d, or null.

jpkcom_acf_jobs_plain_text()

Reduce stored markup to plain text without executing anything.

jpkcom_acf_jobs_plain_text(mixed $value) : string

job_short_description is a textarea with new_lines => 'br', so its stored value is HTML. This turns it back into text. It expands nothing: shortcode expansion is exactly what get_field()'s formatted mode does and what every caller of this function exists to avoid.

Parameters
$value : mixed

Stored value.

Tags
since
1.4.0
Return values
string

Plain text, empty when the value was not a string.

Project ACF post-object values to {id,title}, dropping anything unpublished.

jpkcom_acf_jobs_normalise_related(mixed $value) : array<string|int, mixed>

Never returns a WP_Post. WP_Post implements no JsonSerializable and exposes post_password, post_content and post_status as public properties, so encoding one would publish a related company's plaintext password. ACF resolves post_object fields through acf_get_posts() with post_status 'any', so drafts and private records genuinely arrive here.

Bare integers are accepted and re-resolved: when a translation's ACF key reference row is missing — the case includes/wpml-acf-field-keys-fix.php exists to repair — get_field() returns raw IDs, and every renderer in this repo dereferences ->ID on them.

A bare id is rejected before the lookup when absint() reduces it to less than 1. absint() maps false, '', null, 'abc' and 0 all to 0, and real get_post( 0 ) treats 0 as empty and falls back to the current global post — the same footgun jpkcom_acf_jobs_get_job_data()'s own $post_id < 1 guard exists for. ACF genuinely returns false, not an array, for an unassigned post_object field with allow_null => 1, which both job_company and job_location are, so an unresolved id reaching get_post() unguarded would project the current job as its own employer or location. A password is checked for the same reason the reader's own gate checks one: nothing about a post_object relation implies the related post is public.

Parameters
$value : mixed

Raw ACF value.

since
1.4.0
Return values
array<string|int, mixed>

List of [ 'id' => int, 'title' => string ].

jpkcom_acf_jobs_attachment_url()

Resolve an ACF image value to a single URL.

jpkcom_acf_jobs_attachment_url(mixed $value) : string|null

ACF hands out an image field either as an attachment ID or as an array of roughly thirty keys, depending on the field's return format and on whether the field group is registered at all. Both shapes reduce to one URL here. The full array is deliberately never emitted: it carries the uploader's name, the file path on disk and every registered intermediate size, none of which a job listing needs.

Parameters
$value : mixed

Raw ACF image value.

Tags
since
1.4.0
Return values
string|null

Image URL, or null when the value resolves to no attachment.

jpkcom_acf_jobs_get_job_data()

Read one job as a JSON-serialisable array.

jpkcom_acf_jobs_get_job_data(int $post_id[, bool $full = false ]) : array<string|int, mixed>

The gate is the first act, because no field reader in this plugin has one: schema.php checks the post type, nothing anywhere checks the status, and the existing readers are safe only because the shortcode hands them posts a post_type/post_status query already filtered. A function taking a bare int inherits none of that, and current_user_can( 'read' ) is every logged-in user.

Returns [] for "does not exist" and for "not readable" alike, so the ability on top of it cannot be used to probe which IDs exist.

Parameters
$post_id : int

Job post ID.

$full : bool = false

Whether to include the detail fields (§5.3 of the spec).

Tags
since
1.4.0
Return values
array<string|int, mixed>

The record, or [] when the job is not readable.

jpkcom_acf_jobs_media_size()

Register custom image sizes for job posts

jpkcom_acf_jobs_media_size() : void

Registers three image sizes:

  • jpkcom-acf-job-16x9: 576x324px (16:9, hard crop)
  • jpkcom-acf-job-logo: 512x512px (square, hard crop)
  • jpkcom-acf-job-header: 992x558px (16:9, hard crop)
Tags
since
1.0.0

jpkcom_acf_jobs_image_sizes_to_selector()

Add custom image sizes to media library size selector

jpkcom_acf_jobs_image_sizes_to_selector(array<string|int, string> $sizes) : array<string|int, string>

Makes custom image sizes available in the WordPress media library dropdown when inserting images into posts.

Parameters
$sizes : array<string|int, string>

Existing image size options.

Tags
since
1.0.0
Return values
array<string|int, string>

Modified array with custom sizes added.

jpkcom_acf_jobs_pagination()

Output Bootstrap 5 pagination navigation

jpkcom_acf_jobs_pagination([string|int $pages = '' ][, int $range = 2 ]) : void

Generates numbered pagination with first/last and prev/next controls. Includes proper ARIA labels and accessible markup.

Features:

  • First/Last page links (« »)
  • Previous/Next page links (‹ ›)
  • Numbered page links with range control
  • Active page indicator
  • Disabled state for unavailable actions
Parameters
$pages : string|int = ''

Optional. Total number of pages. Default empty (auto-detect from query).

$range : int = 2

Optional. Number of page links to show on either side of current page. Default 2.

Tags
since
1.0.0
global

int $paged Current page number (set by WordPress).

WP_Query $wp_query WordPress query object.

Return values
void

Outputs HTML directly.

jpkcom_acf_jobs_get_schema_job_posting()

Generate Schema.org JobPosting JSON-LD for a single job post

jpkcom_acf_jobs_get_schema_job_posting([int|null $post_id = null ]) : string

Creates a complete JobPosting schema including:

  • Basic job information (title, description, dates)
  • Employment type (FULL_TIME, PART_TIME, etc.)
  • Hiring organization details and logo
  • Job location(s) with postal addresses
  • Work type (on-site, remote/TELECOMMUTE)
  • Base salary information
  • Application contact/URL
  • Job benefits from taxonomy terms

The schema can be filtered using the 'jpkcom_acf_jobs_schema_job_posting' hook.

Parameters
$post_id : int|null = null

Optional. Post ID of the job post. Default null (uses current post).

Tags
since
1.0.0
Return values
string

JSON-LD formatted string ready for output in <script> tag, or empty string on failure.

jpkcom_acf_jobs_locate_template()

jpkcom_acf_jobs_locate_template(string $template_name) : string|false
Parameters
$template_name : string
Return values
string|false

jpkcom_acf_jobs_locate_template()

Locate template file with override support

jpkcom_acf_jobs_locate_template(string $template_name) : string|false

Searches for template files in this priority order:

  1. Child Theme: /wp-content/themes/your-child-theme/jpkcom-acf-jobs/
  2. Parent Theme: /wp-content/themes/your-theme/jpkcom-acf-jobs/
  3. MU plugin override: /wp-content/mu-plugins/jpkcom-acf-jobs-overrides/templates/
  4. Plugin itself: /wp-content/plugins/jpkcom-acf-jobs/templates/ (or debug-templates/ if WP_DEBUG)
Parameters
$template_name : string

Template filename (e.g., 'single-job.php' or 'partials/job/company.php').

Tags
since
1.0.0
Return values
string|false

Full path to template file if found, false otherwise.

jpkcom_acf_jobs_template_include()

Template loader for singular and archive templates

jpkcom_acf_jobs_template_include(string $template) : string

Intercepts WordPress template_include filter and loads custom templates for job, job_company, and job_location post types (single and archive views).

Parameters
$template : string

Default template path from WordPress.

Tags
since
1.0.0
Return values
string

Template path to use (plugin template or default).

jpkcom_acf_jobs_get_template_part()

Load partial templates with full override support

jpkcom_acf_jobs_get_template_part(string $slug[, string $name = '' ]) : void

Similar to WordPress get_template_part() but uses the plugin's template hierarchy system. Useful for loading reusable template partials.

Example usage: jpkcom_acf_jobs_get_template_part('partials/job/company'); jpkcom_acf_jobs_get_template_part('partials/job/company', 'detailed');

Parameters
$slug : string

Template slug (e.g., 'partials/job/company').

$name : string = ''

Optional. Template name/variation (e.g., 'alternative'). Default empty.

Tags
since
1.0.0

jpkcom_acfjobs_textdomain()

Load plugin text domain for translations

jpkcom_acfjobs_textdomain() : void

Loads translation files from the /languages directory.

Tags
since
1.0.0

jpkcom_acfjobs_locate_file()

Locate file with override support

jpkcom_acfjobs_locate_file(string $filename) : string|null

Searches for a file in multiple locations with priority:

  1. Child theme
  2. Parent theme
  3. MU plugin overrides
  4. Plugin includes directory
Parameters
$filename : string

The filename to locate (without path).

Tags
since
1.0.0
Return values
string|null

Full path to the file if found, null otherwise.

Search results