{"id":19,"date":"2026-09-16T10:28:31","date_gmt":"2026-09-16T10:28:31","guid":{"rendered":"https:\/\/docs.latw.ai\/uncategorized\/php-api\/"},"modified":"2026-09-16T10:28:31","modified_gmt":"2026-09-16T10:28:31","slug":"php-api","status":"publish","type":"post","link":"https:\/\/docs.latw.ai\/pl\/esti-to-wordpress\/php-api\/","title":{"rendered":"PHP API"},"content":{"rendered":"<h1>PHP API: the <code>osfec_*<\/code> functions<\/h1>\n<p>These functions are the plugin&#8217;s public API. They live in <code>includes\/functions.php<\/code>, load on <code>plugins_loaded<\/code> and are<br \/>\navailable anywhere in the theme.<\/p>\n<p>Every function that takes a <code>$post_id<\/code> defaults to the current post in the loop.<\/p>\n<h2>Reading fields<\/h2>\n<h3><code>osfec_field( string $key, ?int $post_id = null ): mixed<\/code><\/h3>\n<p>One meta value, <strong>without<\/strong> the <code>_osfec_<\/code> prefix. The workhorse.<\/p>\n<pre><code class=\"language-php\">$price = osfec_field( 'price' );        \/\/ '540000' or ''\n$city  = osfec_field( 'city', $id );    \/\/ 'Bia\u0142ystok'<\/code><\/pre>\n<p>Missing fields return <code>''<\/code>, because empty values are never stored. See the <a href=\"\/esti-to-wordpress\/data-model#meta\">data model<\/a> for the<br \/>\nfull key list.<\/p>\n<h3><code>osfec_photos( ?int $post_id = null ): array<\/code><\/h3>\n<p>The full-size photo URLs, decoded from JSON. Empty array when the offer has none.<\/p>\n<pre><code class=\"language-php\">foreach ( osfec_photos() as $url ) { \u2026 }<\/code><\/pre>\n<h3><code>osfec_photo_thumb( string $url ): string<\/code><\/h3>\n<p>Converts a full-size Esti URL into its thumbnail variant (<code>_max<\/code> \u2192 <code>_min<\/code>). There is no <code>_small<\/code>.<\/p>\n<h3><code>osfec_agent( ?int $post_id = null ): array<\/code><\/h3>\n<p>The agent behind the offer, merged from two sources: the offer&#8217;s own meta carries the name, e-mail and phone; the photo,<br \/>\nrole and office come from the directory synchronized separately, matched on the Esti agent ID. The directory wins on<br \/>\nevery field it actually carries.<\/p>\n<p>Returns <code>name<\/code>, <code>photo<\/code>, <code>position<\/code>, <code>office<\/code>, <code>email<\/code>, <code>phone<\/code> \u2014 always all six keys, <code>''<\/code> where unknown.<\/p>\n<pre><code class=\"language-php\">$agent = osfec_agent();\n\nif ( $agent['name'] ) {\n    printf( '&lt;a href=\"tel:%s\"&gt;%s&lt;\/a&gt;', esc_attr( $agent['phone'] ), esc_html( $agent['name'] ) );\n}<\/code><\/pre>\n<h2>Formatting<\/h2>\n<h3><code>osfec_format_price( $price, string $currency = '' ): string<\/code><\/h3>\n<p>Localised number plus the currency code. <strong>An empty price returns the translated &#8220;Price on request&#8221;<\/strong>, so guard the<br \/>\nvalue yourself if you need a different empty state.<\/p>\n<pre><code class=\"language-php\">osfec_format_price( '540000', 'PLN' );  \/\/ '540 000 PLN'\nosfec_format_price( '' );               \/\/ 'Price on request'<\/code><\/pre>\n<h3><code>osfec_format_area( $area ): string<\/code><\/h3>\n<p>Localised number plus <code>m\u00b2<\/code>; whole numbers lose their decimals. Empty input returns <code>''<\/code>.<\/p>\n<h3><code>osfec_location( ?int $post_id = null ): string<\/code><\/h3>\n<p>Street, city and commune as one line, empties and duplicates removed: <code>Lipowa, Bia\u0142ystok<\/code>.<\/p>\n<h3><code>osfec_type_label( ?int $post_id = null ): string<\/code><\/h3>\n<p>The short type label, without the bracketed variant: <code>Dzia\u0142ka (Rolna)<\/code> \u2192 <code>Dzia\u0142ka<\/code>. Use <code>osfec_field( 'type_label' )<\/code><br \/>\nfor the full one.<\/p>\n<h3><code>osfec_transaction_label( string $slug ): string<\/code><\/h3>\n<p><code>sale<\/code> \u2192 &#8220;Sale&#8221;, <code>rent<\/code> \u2192 &#8220;Rent&#8221;, <code>purchase<\/code> \u2192 &#8220;Purchase&#8221;, <code>lease<\/code> \u2192 &#8220;Lease&#8221;. Unknown slugs return <code>''<\/code>.<\/p>\n<h3><code>osfec_market_label( string $slug ): string<\/code><\/h3>\n<p><code>primary<\/code> \u2192 &#8220;Primary market&#8221;, <code>secondary<\/code> \u2192 &#8220;Secondary market&#8221;.<\/p>\n<p>Both label functions are translated through the plugin&#8217;s text domain.<\/p>\n<h2>Dictionary-backed attributes<\/h2>\n<p>Some Esti fields arrive as bare numeric IDs. Never print them raw.<\/p>\n<h3><code>osfec_attribute_label( string $key, ?int $post_id = null ): string<\/code><\/h3>\n<p>Reads the meta key and resolves it in one step. <strong>Returns <code>''<\/code> when the dictionary cannot name the ID<\/strong> \u2014 the template<br \/>\nis expected to drop the row in that case.<\/p>\n<pre><code class=\"language-php\">$heating = osfec_attribute_label( 'building_heating' );\n\nif ( '' !== $heating ) {\n    echo esc_html( $heating );\n}<\/code><\/pre>\n<h3><code>osfec_dictionary_label( $id ): string<\/code><\/h3>\n<p>The lower-level version: resolves an ID you already have.<\/p>\n<h2>Building filter controls<\/h2>\n<h3><code>osfec_type_options(): array<\/code><\/h3>\n<p>Type slug \u2192 label, for the offers currently in the catalogue. The slugs match the <code>type<\/code> filter parameter. Cached for 6<br \/>\nhours.<\/p>\n<h3><code>osfec_subtype_options(): array<\/code><\/h3>\n<p>Subtype slug \u2192 label (<code>rolna<\/code> \u2192 <code>Rolna<\/code>). Built the same way, so it fills up after the first synchronization.<\/p>\n<h3><code>osfec_slug_map( string $key ): array<\/code><\/h3>\n<p>Slug \u2192 raw value for any meta key, built from the values actually stored. This is what makes <code>?city=bialystok<\/code> find<br \/>\noffers whose stored city is <code>Bia\u0142ystok<\/code>.<\/p>\n<pre><code class=\"language-php\">$cities = osfec_slug_map( 'city' );   \/\/ [ 'bialystok' =&gt; 'Bia\u0142ystok', \u2026 ]<\/code><\/pre>\n<p>Cached for 6 hours and flushed at the end of every successful run. Each call runs a <code>DISTINCT<\/code> query on <code>wp_postmeta<\/code><br \/>\nwhen the cache is cold, so call it once and reuse the result.<\/p>\n<h2>Templates and page structure<\/h2>\n<h3><code>osfec_get_template( string $name, array $args = array() ): void<\/code><\/h3>\n<p>Includes a template part, letting the theme override it. <code>$name<\/code> is relative to the plugin&#8217;s <code>templates\/<\/code> directory; the<br \/>\ntheme&#8217;s copy lives under <code>osfec\/<\/code> in the theme root. The array reaches the template as <code>$args<\/code>.<br \/>\nSee <a href=\"\/esti-to-wordpress\/template-overrides\">Overriding templates<\/a>.<\/p>\n<pre><code class=\"language-php\">osfec_get_template( 'parts\/card.php', array( 'post_id' =&gt; get_the_ID() ) );<\/code><\/pre>\n<h3><code>osfec_header()<\/code> \/ <code>osfec_footer()<\/code><\/h3>\n<p>Open and close a plugin page with the active theme&#8217;s own chrome.<\/p>\n<p>On a classic theme they are <code>get_header()<\/code> and <code>get_footer()<\/code>. On a block theme they render the theme&#8217;s header and<br \/>\nfooter template parts inside a <code>.wp-site-blocks<\/code> wrapper, and open a <code>&lt;main class=\"osfec-main\"&gt;<\/code> between them \u2014 because<br \/>\n<code>get_header()<\/code> on a block theme falls back to the theme-compatibility stub, an ancient markup block carrying none of the<br \/>\ntheme&#8217;s navigation, colours or fonts.<\/p>\n<p>Use them in any full-page template you write for the offer post type, and always in pairs.<\/p>\n<h2>Class-level entry points you may need<\/h2>\n<p>Not helper functions, but part of the surface a custom UI uses:<\/p>\n<table>\n<thead>\n<tr>\n<th>Call<\/th>\n<th>Purpose<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td><code>OSFEC_Query::request()<\/code><\/td>\n<td>The whitelisted, sanitized GET parameters as an array<\/td>\n<\/tr>\n<tr>\n<td><code>OSFEC_Query::build_args( $request, $base )<\/code><\/td>\n<td>Those parameters as <code>WP_Query<\/code> arguments merged into your own<\/td>\n<\/tr>\n<tr>\n<td><code>OSFEC_Query::archive_url( $request )<\/code><\/td>\n<td>The archive URL with filters applied<\/td>\n<\/tr>\n<tr>\n<td><code>OSFEC_Settings::value( $key, $default )<\/code><\/td>\n<td>One setting<\/td>\n<\/tr>\n<tr>\n<td><code>OSFEC_CPT::POST_TYPE<\/code><\/td>\n<td><code>'osfec_offer'<\/code><\/td>\n<\/tr>\n<tr>\n<td><code>OSFEC_Mapper::thumbnail_url( $url )<\/code><\/td>\n<td>The same conversion <code>osfec_photo_thumb()<\/code> performs<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2>Escaping<\/h2>\n<p>None of these functions escape their output \u2014 they return data, not markup. Escape at the point of printing:<br \/>\n<code>esc_html()<\/code> for text, <code>esc_url()<\/code> for URLs, <code>esc_attr()<\/code> for attributes.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>PHP API: the osfec_* functions These functions are the plugin&#8217;s public API. They live in includes\/functions.php, load on plugins_loaded and are available anywhere in the theme.\u2026<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_latwm_translated_slug":"","footnotes":""},"categories":[2],"tags":[],"class_list":["post-19","post","type-post","status-publish","format-standard","hentry","category-esti-to-wordpress"],"_links":{"self":[{"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/posts\/19","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/comments?post=19"}],"version-history":[{"count":1,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/posts\/19\/revisions"}],"predecessor-version":[{"id":52,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/posts\/19\/revisions\/52"}],"wp:attachment":[{"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/media?parent=19"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/categories?post=19"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/docs.latw.ai\/pl\/wp-json\/wp\/v2\/tags?post=19"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}