PluginProbe
Polylang / 2.0.1
Polylang v2.0.1
3.8.9 3.8.8 3.8.7 3.8.6 3.8.5 3.8.4 3.8.3 2.7 2.7.0.1 2.7.1 2.7.2 2.7.3 2.7.4 2.8 2.8.1 2.8.2 2.8.3 2.8.4 2.9 2.9.1 2.9.2 3.0 3.0.1 3.0.2 3.0.3 All 233 releases
polylang / include / model.php

model.php in Polylang 2.0.1, at include/model.php

629 lines 21.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Setups the language and translations model based on WordPress taxonomies
5 *
6 * @since 1.2
7 */
8 class PLL_Model {
9 public $cache; // our internal non persistent cache object
10 public $options;
11 public $post, $term; // translated objects models
12
13 /**
14 * Constructor
15 * setups translated objects sub models
16 * setups filters and actions
17 *
18 * @since 1.2
19 *
20 * @param array $options Polylang options
21 */
22 public function __construct( &$options ) {
23 $this->options = &$options;
24
25 $this->cache = new PLL_Cache();
26 $this->post = new PLL_Translated_Post( $this ); // translated post sub model
27 $this->term = new PLL_Translated_Term( $this ); // translated term sub model
28
29 // we need to clean languages cache when editing a language and when modifying the permalink structure
30 add_action( 'edited_term_taxonomy', array( $this, 'clean_languages_cache' ), 10, 2 );
31 add_action( 'update_option_permalink_structure', array( $this, 'clean_languages_cache' ) );
32 add_action( 'update_option_siteurl', array( $this, 'clean_languages_cache' ) );
33 add_action( 'update_option_home', array( $this, 'clean_languages_cache' ) );
34
35 // just in case someone would like to display the language description ;- )
36 add_filter( 'language_description', '__return_empty_string' );
37 }
38
39 /**
40 * Returns the list of available languages
41 * caches the list in a db transient ( except flags ), unless PLL_CACHE_LANGUAGES is set to false
42 * caches the list ( with flags ) in the private property $languages
43 *
44 * list of parameters accepted in $args:
45 *
46 * hide_empty => hides languages with no posts if set to true ( defaults to false )
47 * fields => return only that field if set ( see PLL_Language for a list of fields )
48 *
49 * @since 0.1
50 *
51 * @param array $args
52 * @return array|string|int list of PLL_Language objects or PLL_Language object properties
53 */
54 public function get_languages_list( $args = array() ) {
55 if ( false === $languages = $this->cache->get( 'languages' ) ) {
56
57 // create the languages from taxonomies
58 if ( ( defined( 'PLL_CACHE_LANGUAGES' ) && ! PLL_CACHE_LANGUAGES ) || false === ( $languages = get_transient( 'pll_languages_list' ) ) ) {
59 $languages = get_terms( 'language', array( 'hide_empty' => false, 'orderby' => 'term_group' ) );
60 $languages = empty( $languages ) || is_wp_error( $languages ) ? array() : $languages;
61
62 $term_languages = get_terms( 'term_language', array( 'hide_empty' => false ) );
63 $term_languages = empty( $term_languages ) || is_wp_error( $term_languages ) ?
64 array() : array_combine( wp_list_pluck( $term_languages, 'slug' ), $term_languages );
65
66 if ( ! empty( $languages ) && ! empty( $term_languages ) ) {
67 // don't use array_map + create_function to instantiate an autoloaded class as it breaks badly in old versions of PHP
68 foreach ( $languages as $k => $v ) {
69 $languages[ $k ] = new PLL_Language( $v, $term_languages[ 'pll_' . $v->slug ] );
70 }
71
72 // we will need the languages list to allow its access in the filter below
73 $this->cache->set( 'languages', $languages );
74
75 /**
76 * Filter the list of languages *before* it is stored in the persistent cache
77 * /!\ this filter is fired *before* the $polylang object is available
78 *
79 * @since 1.7.5
80 *
81 * @param array $languages the list of language objects
82 * @param object $model PLL_Model object
83 */
84 $languages = apply_filters( 'pll_languages_list', $languages, $this );
85
86 // don't store directly objects as it badly break with some hosts ( GoDaddy ) due to race conditions when using object cache
87 // thanks to captin411 for catching this!
88 // see https://wordpress.org/support/topic/fatal-error-pll_model_languages_list?replies=8#post-6782255;
89 set_transient( 'pll_languages_list', array_map( 'get_object_vars', $languages ) );
90 }
91 else {
92 $languages = array(); // in case something went wrong
93 }
94 }
95
96 // create the languages directly from arrays stored in transients
97 else {
98 foreach ( $languages as $k => $v ) {
99 $languages[ $k ] = new PLL_Language( $v );
100 }
101 }
102
103 // custom flags
104 if ( ! PLL_ADMIN ) {
105 foreach ( $languages as $language ) {
106 $language->set_custom_flag();
107 }
108 }
109
110 /**
111 * Filter the list of languages *after* it is stored in the persistent cache
112 * /!\ this filter is fired *before* the $polylang object is available
113 *
114 * @since 1.8
115 *
116 * @param array $languages the list of language objects
117 */
118 $languages = apply_filters( 'pll_after_languages_cache', $languages );
119 $this->cache->set( 'languages', $languages );
120 }
121
122 $args = wp_parse_args( $args, array( 'hide_empty' => false ) );
123
124 // remove empty languages if requested
125 if ( $args['hide_empty'] ) {
126 $languages = wp_list_filter( $languages, array( 'count' => 0 ), 'NOT' );
127 }
128
129 return empty( $args['fields'] ) ? $languages : wp_list_pluck( $languages, $args['fields'] );
130 }
131
132 /**
133 * Cleans language cache
134 * can be called directly with no parameter
135 * called by the 'edited_term_taxonomy' filter with 2 parameters when count needs to be updated
136 *
137 * @since 1.2
138 *
139 * @param int $term not used
140 * @param string $taxonomy taxonomy name
141 */
142 public function clean_languages_cache( $term = 0, $taxonomy = null ) {
143 // depending on WP version, the action is passed an object or a string
144 // backward compatibility with WP < 4.2
145 if ( ! empty( $taxonomy ) && is_object( $taxonomy ) ) {
146 $taxonomy = $taxonomy->name;
147 }
148
149 if ( empty( $taxonomy ) || 'language' == $taxonomy ) {
150 delete_transient( 'pll_languages_list' );
151 $this->cache->clean();
152 }
153 }
154
155 /**
156 * Returns the language by its term_id, tl_term_id, slug or locale
157 *
158 * @since 0.1
159 *
160 * @param int|string term_id, tl_term_id, slug or locale of the queried language
161 * @return object|bool PLL_Language object, false if no language found
162 */
163 public function get_language( $value ) {
164 if ( is_object( $value ) ) {
165 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // will force cast to PLL_Language
166 }
167
168 if ( false === $return = $this->cache->get( 'language:' . $value ) ) {
169 foreach ( $this->get_languages_list() as $lang ) {
170 $this->cache->set( 'language:' . $lang->term_id, $lang );
171 $this->cache->set( 'language:' . $lang->tl_term_id, $lang );
172 $this->cache->set( 'language:' . $lang->slug, $lang );
173 $this->cache->set( 'language:' . $lang->locale, $lang );
174 }
175 $return = $this->cache->get( 'language:' . $value );
176 }
177
178 return $return;
179 }
180
181 /**
182 * Adds terms clauses to get_terms to filter them by languages - used in both frontend and admin
183 *
184 * @since 1.2
185 *
186 * @param array $clauses the list of sql clauses in terms query
187 * @param object $lang PLL_Language object
188 * @return array modifed list of clauses
189 */
190 public function terms_clauses( $clauses, $lang ) {
191 if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) {
192 $clauses['join'] .= $this->term->join_clause();
193 $clauses['where'] .= $this->term->where_clause( $lang );
194 }
195 return $clauses;
196 }
197
198 /**
199 * Returns post types that need to be translated
200 * the post types list is cached for better better performance
201 * wait for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php
202 *
203 * @since 1.2
204 *
205 * @param bool $filter true if we should return only valid registered post types
206 * @return array post type names for which Polylang manages languages and translations
207 */
208 public function get_translated_post_types( $filter = true ) {
209 if ( false === $post_types = $this->cache->get( 'post_types' ) ) {
210 $post_types = array( 'post' => 'post', 'page' => 'page' );
211
212 if ( ! empty( $this->options['media_support'] ) ) {
213 $post_types['attachment'] = 'attachment';
214 }
215
216 if ( ! empty( $this->options['post_types'] ) && is_array( $this->options['post_types'] ) ) {
217 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
218 }
219
220 /**
221 * Filter the list of post types available for translation.
222 * The default are post types which have the parameter ‘public’ set to true.
223 * The filter must be added soon in the WordPress loading process:
224 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
225 *
226 * @since 0.8
227 *
228 * @param array $post_types list of post type names
229 * @param bool $is_settings true when displaying the list of custom post types in Polylang settings
230 */
231 $post_types = apply_filters( 'pll_get_post_types', $post_types, false );
232
233 if ( did_action( 'after_setup_theme' ) ) {
234 $this->cache->set( 'post_types', $post_types );
235 }
236 }
237
238 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
239 }
240
241 /**
242 * Returns true if Polylang manages languages and translations for this post type
243 *
244 * @since 1.2
245 *
246 * @param string|array $post_type post type name or array of post type names
247 * @return bool
248 */
249 public function is_translated_post_type( $post_type ) {
250 $post_types = $this->get_translated_post_types( false );
251 return ( is_array( $post_type ) && array_intersect( $post_type, $post_types ) || in_array( $post_type, $post_types ) );
252 }
253
254 /**
255 * Return taxonomies that need to be translated
256 *
257 * @since 1.2
258 *
259 * @param bool $filter true if we should return only valid registered taxonmies
260 * @return array array of registered taxonomy names for which Polylang manages languages and translations
261 */
262 public function get_translated_taxonomies( $filter = true ) {
263 if ( false === $taxonomies = $this->cache->get( 'taxonomies' ) ) {
264 $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
265
266 if ( ! empty( $this->options['taxonomies'] ) && is_array( $this->options['taxonomies'] ) ) {
267 $taxonomies = array_merge( $taxonomies, array_combine( $this->options['taxonomies'], $this->options['taxonomies'] ) );
268 }
269
270 /**
271 * Filter the list of taxonomies available for translation.
272 * The default are taxonomies which have the parameter ‘public’ set to true.
273 * The filter must be added soon in the WordPress loading process:
274 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
275 *
276 * @since 0.8
277 *
278 * @param array $taxonomies list of taxonomy names
279 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
280 */
281 $taxonomies = apply_filters( 'pll_get_taxonomies', $taxonomies, false );
282 if ( did_action( 'after_setup_theme' ) ) {
283 $this->cache->set( 'taxonomies', $taxonomies );
284 }
285 }
286
287 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
288 }
289
290 /**
291 * Returns true if Polylang manages languages and translations for this taxonomy
292 *
293 * @since 1.2
294 *
295 * @param string|array $tax taxonomy name or array of taxonomy names
296 * @return bool
297 */
298 public function is_translated_taxonomy( $tax ) {
299 $taxonomies = $this->get_translated_taxonomies( false );
300 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
301 }
302
303 /**
304 * Check if translated taxonomy is queried
305 * Compatible with nested queries introduced in WP 4.1
306 * @see https://wordpress.org/support/topic/tax_query-bug
307 *
308 * @since 1.7
309 *
310 * @param array $tax_queries
311 * @return bool
312 */
313 public function have_translated_taxonomy( $tax_queries ) {
314 foreach ( $tax_queries as $tax_query ) {
315 if ( isset( $tax_query['taxonomy'] ) && $this->is_translated_taxonomy( $tax_query['taxonomy'] ) && ! ( isset( $tax_query['operator'] ) && 'NOT IN' === $tax_query['operator'] ) ) {
316 return true;
317 }
318
319 // Nested queries
320 elseif ( is_array( $tax_query ) && $this->have_translated_taxonomy( $tax_query ) ) {
321 return true;
322 }
323 }
324
325 return false;
326 }
327
328 /**
329 * Return taxonomies that need to be filtered ( post_format like )
330 *
331 * @since 1.7
332 *
333 * @param bool $filter true if we should return only valid registered taxonomies
334 * @return array array of registered taxonomy names
335 */
336 public function get_filtered_taxonomies( $filter = true ) {
337 if ( did_action( 'after_setup_theme' ) ) {
338 static $taxonomies = null;
339 }
340
341 if ( empty( $taxonomies ) ) {
342 $taxonomies = array( 'post_format' => 'post_format' );
343
344 /**
345 * Filter the list of taxonomies not translatable but filtered by language.
346 * Includes only the post format by default
347 * The filter must be added soon in the WordPress loading process:
348 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
349 *
350 * @since 1.7
351 *
352 * @param array $taxonomies list of taxonomy names
353 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
354 */
355 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
356 }
357
358 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
359 }
360
361 /**
362 * Returns true if Polylang filters this taxonomy per language
363 *
364 * @since 1.7
365 *
366 * @param string|array $tax taxonomy name or array of taxonomy names
367 * @return bool
368 */
369 public function is_filtered_taxonomy( $tax ) {
370 $taxonomies = $this->get_filtered_taxonomies( false );
371 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
372 }
373
374 /**
375 * Returns the query vars of all filtered taxonomies
376 *
377 * @since 1.7
378 *
379 * @return array
380 */
381 public function get_filtered_taxonomies_query_vars() {
382 $query_vars = array();
383 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
384 $tax = get_taxonomy( $filtered_tax );
385 $query_vars[] = $tax->query_var;
386 }
387 return $query_vars;
388 }
389
390 /**
391 * Create a default category for a language
392 *
393 * @since 1.2
394 *
395 * @param object|string|int $lang language
396 */
397 public function create_default_category( $lang ) {
398 $lang = $this->get_language( $lang );
399
400 // create a new category
401 // FIXME this is translated in admin language when we would like it in $lang
402 $cat_name = __( 'Uncategorized' );
403 $cat_slug = sanitize_title( $cat_name . '-' . $lang->slug );
404 $cat = wp_insert_term( $cat_name, 'category', array( 'slug' => $cat_slug ) );
405
406 // check that the category was not previously created ( in case the language was deleted and recreated )
407 $cat = isset( $cat->error_data['term_exists'] ) ? $cat->error_data['term_exists'] : $cat['term_id'];
408
409 // set language
410 $this->term->set_language( (int) $cat, $lang );
411
412 // this is a translation of the default category
413 $default = (int) get_option( 'default_category' );
414 $translations = $this->term->get_translations( $default );
415 if ( empty( $translations ) ) {
416 if ( $lg = $this->term->get_language( $default ) ) {
417 $translations[ $lg->slug ] = $default;
418 }
419 else {
420 $translations = array();
421 }
422 }
423
424 $this->term->save_translations( (int) $cat, $translations );
425 }
426
427 /**
428 * It is possible to have several terms with the same name in the same taxonomy ( one per language )
429 * but the native term_exists will return true even if only one exists
430 * so here the function adds the language parameter
431 *
432 * @since 1.4
433 *
434 * @param string $term_name the term name
435 * @param string $taxonomy taxonomy name
436 * @param int $parent parent term id
437 * @param string|object $language the language slug or object
438 * @return null|int the term_id of the found term
439 */
440 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
441 global $wpdb;
442
443 $term_name = trim( wp_unslash( $term_name ) );
444
445 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
446 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
447 $join .= $this->term->join_clause();
448 $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name );
449 $where .= $this->term->where_clause( $this->get_language( $language ) );
450
451 if ( $parent > 0 ) {
452 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
453 }
454
455 return $wpdb->get_var( $select . $join . $where );
456 }
457
458 /**
459 * Gets the number of posts per language in a date, author or post type archive
460 *
461 * @since 1.2
462 *
463 * @param object $lang
464 * @param array $q WP_Query arguments ( accepted: post_type, m, year, monthnum, day, author, author_name, post_format )
465 * @return int
466 */
467 public function count_posts( $lang, $q = array() ) {
468 global $wpdb;
469
470 $q = wp_parse_args( $q, array( 'post_type' => 'post' ) );
471
472 if ( ! is_array( $q['post_type'] ) ) {
473 $q['post_type'] = array( $q['post_type'] );
474 }
475
476 foreach ( $q['post_type'] as $key => $type ) {
477 if ( ! post_type_exists( $type ) ) {
478 unset( $q['post_type'][ $key ] );
479 }
480 }
481
482 if ( empty( $q['post_type'] ) ) {
483 $q['post_type'] = array( 'post' ); // we *need* a post type
484 }
485
486 $cache_key = md5( serialize( $q ) );
487 $counts = wp_cache_get( $cache_key, 'pll_count_posts' );
488
489 if ( false === $counts ) {
490 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}";
491 $join = $this->post->join_clause();
492 $where = " WHERE post_status = 'publish'";
493 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type IN ( '%s' )", join( "', '", $q['post_type'] ) );
494 $where .= $this->post->where_clause( $this->get_languages_list() );
495 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
496
497 if ( ! empty( $q['m'] ) ) {
498 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
499 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) );
500 if ( strlen( $q['m'] ) > 5 ) {
501 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) );
502 }
503 if ( strlen( $q['m'] ) > 7 ) {
504 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) );
505 }
506 }
507
508 if ( ! empty( $q['year'] ) ) {
509 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] );
510 }
511
512 if ( ! empty( $q['monthnum'] ) ) {
513 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] );
514 }
515
516 if ( ! empty( $q['day'] ) ) {
517 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] );
518 }
519
520 if ( ! empty( $q['author_name'] ) ) {
521 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
522 if ( $author ) {
523 $q['author'] = $author->ID;
524 }
525 }
526
527 if ( ! empty( $q['author'] ) ) {
528 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] );
529 }
530
531 // filtered taxonomies ( post_format )
532 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
533
534 if ( ! empty( $q[ $tax_qv ] ) ) {
535 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID";
536 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
537 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
538 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
539 }
540 }
541
542 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
543 foreach ( (array) $res as $row ) {
544 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
545 }
546
547 wp_cache_set( $cache_key, $counts, 'pll_count_posts' );
548 }
549
550 return empty( $counts[ $lang->term_taxonomy_id ] ) ? 0 : $counts[ $lang->term_taxonomy_id ];
551 }
552
553 /**
554 * Setup the links model based on options
555 *
556 * @since 1.2
557 *
558 * @return object implementing "links_model interface"
559 */
560 public function get_links_model() {
561 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
562 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
563 return new $class( $this );
564 }
565
566 /**
567 * Some backward compatibility with Polylang < 1.8
568 * allows for example to call $polylang->model->get_post_languages( $post_id ) instead of $polylang->model->post->get_language( $post_id )
569 * this works but should be slower than the direct call, thus an error is triggered in debug mode
570 *
571 * @since 1.8
572 *
573 * @param string $func function name
574 * @param array $args function arguments
575 */
576 public function __call( $func, $args ) {
577 $f = $func;
578
579 switch ( $func ) {
580 case 'get_object_term':
581 $o = false === strpos( $args[1], 'term' ) ? 'post' : 'term';
582 break;
583
584 case 'save_translations':
585 case 'delete_translation':
586 case 'get_translations':
587 case 'get_translation':
588 case 'join_clause':
589 $o = ( 'post' == $args[0] || $this->is_translated_post_type( $args[0] ) ) ? 'post' : ( 'term' == $args[0] || $this->is_translated_taxonomy( $args[0] ) ? 'term' : false );
590 unset( $args[0] );
591 break;
592
593 case 'set_post_language':
594 case 'get_post_language':
595 case 'set_term_language':
596 case 'get_term_language':
597 case 'delete_term_language':
598 case 'get_post':
599 case 'get_term':
600 $str = explode( '_', $func );
601 $f = empty( $str[2] ) ? $str[0] : $str[0] . '_' . $str[2];
602 $o = $str[1];
603 break;
604
605 case 'where_clause':
606 case 'get_objects_in_language':
607 $o = $args[1];
608 unset( $args[1] );
609 break;
610 }
611
612 if ( ! empty( $o ) && is_object( $this->$o ) && method_exists( $this->$o, $f ) ) {
613 if ( WP_DEBUG ) {
614 $debug = debug_backtrace();
615 $i = 1 + empty( $debug[1]['line'] ); // the file and line are in $debug[2] if the function was called using call_user_func
616
617 trigger_error( sprintf(
618 '%1$s was called incorrectly in %4$s on line %5$s: the call to $polylang->model->%1$s() has been deprecated in Polylang 1.8, use PLL()->model->%2$s->%3$s() instead.' . "\nError handler",
619 $func, $o, $f, $debug[ $i ]['file'], $debug[ $i ]['line']
620 ) );
621 }
622 return call_user_func_array( array( $this->$o, $f ), $args );
623 }
624
625 $debug = debug_backtrace();
626 trigger_error( sprintf( 'Call to undefined function PLL()->model->%1$s() in %2$s on line %3$s' . "\nError handler", $func, $debug[0]['file'], $debug[0]['line'] ), E_USER_ERROR );
627 }
628 }
629