PluginProbe
Polylang / 2.1.1
Polylang v2.1.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.1.1, at include/model.php

634 lines 21.7 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 if ( empty( $taxonomy ) || 'language' == $taxonomy ) {
144 delete_transient( 'pll_languages_list' );
145 $this->cache->clean();
146 }
147 }
148
149 /**
150 * Returns the language by its term_id, tl_term_id, slug or locale
151 *
152 * @since 0.1
153 *
154 * @param int|string term_id, tl_term_id, slug or locale of the queried language
155 * @return object|bool PLL_Language object, false if no language found
156 */
157 public function get_language( $value ) {
158 if ( is_object( $value ) ) {
159 return $value instanceof PLL_Language ? $value : $this->get_language( $value->term_id ); // will force cast to PLL_Language
160 }
161
162 if ( false === $return = $this->cache->get( 'language:' . $value ) ) {
163 foreach ( $this->get_languages_list() as $lang ) {
164 $this->cache->set( 'language:' . $lang->term_id, $lang );
165 $this->cache->set( 'language:' . $lang->tl_term_id, $lang );
166 $this->cache->set( 'language:' . $lang->slug, $lang );
167 $this->cache->set( 'language:' . $lang->locale, $lang );
168 }
169 $return = $this->cache->get( 'language:' . $value );
170 }
171
172 return $return;
173 }
174
175 /**
176 * Adds terms clauses to get_terms to filter them by languages - used in both frontend and admin
177 *
178 * @since 1.2
179 *
180 * @param array $clauses the list of sql clauses in terms query
181 * @param object $lang PLL_Language object
182 * @return array modifed list of clauses
183 */
184 public function terms_clauses( $clauses, $lang ) {
185 if ( ! empty( $lang ) && false === strpos( $clauses['join'], 'pll_tr' ) ) {
186 $clauses['join'] .= $this->term->join_clause();
187 $clauses['where'] .= $this->term->where_clause( $lang );
188 }
189 return $clauses;
190 }
191
192 /**
193 * Returns post types that need to be translated
194 * the post types list is cached for better better performance
195 * wait for 'after_setup_theme' to apply the cache to allow themes adding the filter in functions.php
196 *
197 * @since 1.2
198 *
199 * @param bool $filter true if we should return only valid registered post types
200 * @return array post type names for which Polylang manages languages and translations
201 */
202 public function get_translated_post_types( $filter = true ) {
203 if ( false === $post_types = $this->cache->get( 'post_types' ) ) {
204 $post_types = array( 'post' => 'post', 'page' => 'page' );
205
206 if ( ! empty( $this->options['media_support'] ) ) {
207 $post_types['attachment'] = 'attachment';
208 }
209
210 if ( ! empty( $this->options['post_types'] ) && is_array( $this->options['post_types'] ) ) {
211 $post_types = array_merge( $post_types, array_combine( $this->options['post_types'], $this->options['post_types'] ) );
212 }
213
214 /**
215 * Filter the list of post types available for translation.
216 * The default are post types which have the parameter ‘public’ set to true.
217 * The filter must be added soon in the WordPress loading process:
218 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
219 *
220 * @since 0.8
221 *
222 * @param array $post_types list of post type names
223 * @param bool $is_settings true when displaying the list of custom post types in Polylang settings
224 */
225 $post_types = apply_filters( 'pll_get_post_types', $post_types, false );
226
227 if ( did_action( 'after_setup_theme' ) ) {
228 $this->cache->set( 'post_types', $post_types );
229 }
230 }
231
232 return $filter ? array_intersect( $post_types, get_post_types() ) : $post_types;
233 }
234
235 /**
236 * Returns true if Polylang manages languages and translations for this post type
237 *
238 * @since 1.2
239 *
240 * @param string|array $post_type post type name or array of post type names
241 * @return bool
242 */
243 public function is_translated_post_type( $post_type ) {
244 $post_types = $this->get_translated_post_types( false );
245 return ( is_array( $post_type ) && array_intersect( $post_type, $post_types ) || in_array( $post_type, $post_types ) );
246 }
247
248 /**
249 * Return taxonomies that need to be translated
250 *
251 * @since 1.2
252 *
253 * @param bool $filter true if we should return only valid registered taxonmies
254 * @return array array of registered taxonomy names for which Polylang manages languages and translations
255 */
256 public function get_translated_taxonomies( $filter = true ) {
257 if ( false === $taxonomies = $this->cache->get( 'taxonomies' ) ) {
258 $taxonomies = array( 'category' => 'category', 'post_tag' => 'post_tag' );
259
260 if ( ! empty( $this->options['taxonomies'] ) && is_array( $this->options['taxonomies'] ) ) {
261 $taxonomies = array_merge( $taxonomies, array_combine( $this->options['taxonomies'], $this->options['taxonomies'] ) );
262 }
263
264 /**
265 * Filter the list of taxonomies available for translation.
266 * The default are taxonomies which have the parameter ‘public’ set to true.
267 * The filter must be added soon in the WordPress loading process:
268 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
269 *
270 * @since 0.8
271 *
272 * @param array $taxonomies list of taxonomy names
273 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
274 */
275 $taxonomies = apply_filters( 'pll_get_taxonomies', $taxonomies, false );
276 if ( did_action( 'after_setup_theme' ) ) {
277 $this->cache->set( 'taxonomies', $taxonomies );
278 }
279 }
280
281 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
282 }
283
284 /**
285 * Returns true if Polylang manages languages and translations for this taxonomy
286 *
287 * @since 1.2
288 *
289 * @param string|array $tax taxonomy name or array of taxonomy names
290 * @return bool
291 */
292 public function is_translated_taxonomy( $tax ) {
293 $taxonomies = $this->get_translated_taxonomies( false );
294 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
295 }
296
297 /**
298 * Check if translated taxonomy is queried
299 * Compatible with nested queries introduced in WP 4.1
300 * @see https://wordpress.org/support/topic/tax_query-bug
301 *
302 * @since 1.7
303 *
304 * @param array $tax_queries
305 * @return bool
306 */
307 public function have_translated_taxonomy( $tax_queries ) {
308 foreach ( $tax_queries as $tax_query ) {
309 if ( isset( $tax_query['taxonomy'] ) && $this->is_translated_taxonomy( $tax_query['taxonomy'] ) && ! ( isset( $tax_query['operator'] ) && 'NOT IN' === $tax_query['operator'] ) ) {
310 return true;
311 }
312
313 // Nested queries
314 elseif ( is_array( $tax_query ) && $this->have_translated_taxonomy( $tax_query ) ) {
315 return true;
316 }
317 }
318
319 return false;
320 }
321
322 /**
323 * Return taxonomies that need to be filtered ( post_format like )
324 *
325 * @since 1.7
326 *
327 * @param bool $filter true if we should return only valid registered taxonomies
328 * @return array array of registered taxonomy names
329 */
330 public function get_filtered_taxonomies( $filter = true ) {
331 if ( did_action( 'after_setup_theme' ) ) {
332 static $taxonomies = null;
333 }
334
335 if ( empty( $taxonomies ) ) {
336 $taxonomies = array( 'post_format' => 'post_format' );
337
338 /**
339 * Filter the list of taxonomies not translatable but filtered by language.
340 * Includes only the post format by default
341 * The filter must be added soon in the WordPress loading process:
342 * in a function hooked to ‘plugins_loaded’ or directly in functions.php for themes.
343 *
344 * @since 1.7
345 *
346 * @param array $taxonomies list of taxonomy names
347 * @param bool $is_settings true when displaying the list of custom taxonomies in Polylang settings
348 */
349 $taxonomies = apply_filters( 'pll_filtered_taxonomies', $taxonomies, false );
350 }
351
352 return $filter ? array_intersect( $taxonomies, get_taxonomies() ) : $taxonomies;
353 }
354
355 /**
356 * Returns true if Polylang filters this taxonomy per language
357 *
358 * @since 1.7
359 *
360 * @param string|array $tax taxonomy name or array of taxonomy names
361 * @return bool
362 */
363 public function is_filtered_taxonomy( $tax ) {
364 $taxonomies = $this->get_filtered_taxonomies( false );
365 return ( is_array( $tax ) && array_intersect( $tax, $taxonomies ) || in_array( $tax, $taxonomies ) );
366 }
367
368 /**
369 * Returns the query vars of all filtered taxonomies
370 *
371 * @since 1.7
372 *
373 * @return array
374 */
375 public function get_filtered_taxonomies_query_vars() {
376 $query_vars = array();
377 foreach ( $this->get_filtered_taxonomies() as $filtered_tax ) {
378 $tax = get_taxonomy( $filtered_tax );
379 $query_vars[] = $tax->query_var;
380 }
381 return $query_vars;
382 }
383
384 /**
385 * Create a default category for a language
386 *
387 * @since 1.2
388 *
389 * @param object|string|int $lang language
390 */
391 public function create_default_category( $lang ) {
392 $lang = $this->get_language( $lang );
393
394 // create a new category
395 // FIXME this is translated in admin language when we would like it in $lang
396 $cat_name = __( 'Uncategorized' );
397 $cat_slug = sanitize_title( $cat_name . '-' . $lang->slug );
398 $cat = wp_insert_term( $cat_name, 'category', array( 'slug' => $cat_slug ) );
399
400 // check that the category was not previously created ( in case the language was deleted and recreated )
401 $cat = isset( $cat->error_data['term_exists'] ) ? $cat->error_data['term_exists'] : $cat['term_id'];
402
403 // set language
404 $this->term->set_language( (int) $cat, $lang );
405
406 // this is a translation of the default category
407 $default = (int) get_option( 'default_category' );
408 $translations = $this->term->get_translations( $default );
409 if ( empty( $translations ) ) {
410 if ( $lg = $this->term->get_language( $default ) ) {
411 $translations[ $lg->slug ] = $default;
412 }
413 else {
414 $translations = array();
415 }
416 }
417
418 $this->term->save_translations( (int) $cat, $translations );
419 }
420
421 /**
422 * It is possible to have several terms with the same name in the same taxonomy ( one per language )
423 * but the native term_exists will return true even if only one exists
424 * so here the function adds the language parameter
425 *
426 * @since 1.4
427 *
428 * @param string $term_name the term name
429 * @param string $taxonomy taxonomy name
430 * @param int $parent parent term id
431 * @param string|object $language the language slug or object
432 * @return null|int the term_id of the found term
433 */
434 public function term_exists( $term_name, $taxonomy, $parent, $language ) {
435 global $wpdb;
436
437 $term_name = trim( wp_unslash( $term_name ) );
438
439 $select = "SELECT t.term_id FROM $wpdb->terms AS t";
440 $join = " INNER JOIN $wpdb->term_taxonomy AS tt ON t.term_id = tt.term_id";
441 $join .= $this->term->join_clause();
442 $where = $wpdb->prepare( ' WHERE tt.taxonomy = %s AND t.name = %s', $taxonomy, $term_name );
443 $where .= $this->term->where_clause( $this->get_language( $language ) );
444
445 if ( $parent > 0 ) {
446 $where .= $wpdb->prepare( ' AND tt.parent = %d', $parent );
447 }
448
449 return $wpdb->get_var( $select . $join . $where );
450 }
451
452 /**
453 * Gets the number of posts per language in a date, author or post type archive
454 *
455 * @since 1.2
456 *
457 * @param object $lang
458 * @param array $q WP_Query arguments ( accepted: post_type, m, year, monthnum, day, author, author_name, post_format )
459 * @return int
460 */
461 public function count_posts( $lang, $q = array() ) {
462 global $wpdb;
463
464 $q = wp_parse_args( $q, array( 'post_type' => 'post' ) );
465
466 if ( ! is_array( $q['post_type'] ) ) {
467 $q['post_type'] = array( $q['post_type'] );
468 }
469
470 foreach ( $q['post_type'] as $key => $type ) {
471 if ( ! post_type_exists( $type ) ) {
472 unset( $q['post_type'][ $key ] );
473 }
474 }
475
476 if ( empty( $q['post_type'] ) ) {
477 $q['post_type'] = array( 'post' ); // we *need* a post type
478 }
479
480 $cache_key = md5( serialize( $q ) );
481 $counts = wp_cache_get( $cache_key, 'pll_count_posts' );
482
483 if ( false === $counts ) {
484 $select = "SELECT pll_tr.term_taxonomy_id, COUNT( * ) AS num_posts FROM {$wpdb->posts}";
485 $join = $this->post->join_clause();
486 $where = " WHERE post_status = 'publish'";
487 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_type IN ( '%s' )", join( "', '", $q['post_type'] ) );
488 $where .= $this->post->where_clause( $this->get_languages_list() );
489 $groupby = ' GROUP BY pll_tr.term_taxonomy_id';
490
491 if ( ! empty( $q['m'] ) ) {
492 $q['m'] = '' . preg_replace( '|[^0-9]|', '', $q['m'] );
493 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 0, 4 ) );
494 if ( strlen( $q['m'] ) > 5 ) {
495 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 4, 2 ) );
496 }
497 if ( strlen( $q['m'] ) > 7 ) {
498 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", substr( $q['m'], 6, 2 ) );
499 }
500 }
501
502 if ( ! empty( $q['year'] ) ) {
503 $where .= $wpdb->prepare( " AND YEAR( {$wpdb->posts}.post_date ) = %d", $q['year'] );
504 }
505
506 if ( ! empty( $q['monthnum'] ) ) {
507 $where .= $wpdb->prepare( " AND MONTH( {$wpdb->posts}.post_date ) = %d", $q['monthnum'] );
508 }
509
510 if ( ! empty( $q['day'] ) ) {
511 $where .= $wpdb->prepare( " AND DAYOFMONTH( {$wpdb->posts}.post_date ) = %d", $q['day'] );
512 }
513
514 if ( ! empty( $q['author_name'] ) ) {
515 $author = get_user_by( 'slug', sanitize_title_for_query( $q['author_name'] ) );
516 if ( $author ) {
517 $q['author'] = $author->ID;
518 }
519 }
520
521 if ( ! empty( $q['author'] ) ) {
522 $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_author = %d", $q['author'] );
523 }
524
525 // filtered taxonomies ( post_format )
526 foreach ( $this->get_filtered_taxonomies_query_vars() as $tax_qv ) {
527
528 if ( ! empty( $q[ $tax_qv ] ) ) {
529 $join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON tr.object_id = {$wpdb->posts}.ID";
530 $join .= " INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_taxonomy_id = tr.term_taxonomy_id";
531 $join .= " INNER JOIN {$wpdb->terms} AS t ON t.term_id = tt.term_id";
532 $where .= $wpdb->prepare( ' AND t.slug = %s', $q[ $tax_qv ] );
533 }
534 }
535
536 $res = $wpdb->get_results( $select . $join . $where . $groupby, ARRAY_A );
537 foreach ( (array) $res as $row ) {
538 $counts[ $row['term_taxonomy_id'] ] = $row['num_posts'];
539 }
540
541 wp_cache_set( $cache_key, $counts, 'pll_count_posts' );
542 }
543
544 return empty( $counts[ $lang->term_taxonomy_id ] ) ? 0 : $counts[ $lang->term_taxonomy_id ];
545 }
546
547 /**
548 * Setup the links model based on options
549 *
550 * @since 1.2
551 *
552 * @return object implementing "links_model interface"
553 */
554 public function get_links_model() {
555 $c = array( 'Directory', 'Directory', 'Subdomain', 'Domain' );
556 $class = get_option( 'permalink_structure' ) ? 'PLL_Links_' . $c[ $this->options['force_lang'] ] : 'PLL_Links_Default';
557
558 /**
559 * Filter the links model class to use
560 * /!\ this filter is fired *before* the $polylang object is available
561 *
562 * @since 2.1.1
563 *
564 * @param string $class A class name: PLL_Links_Default, PLL_Links_Directory, PLL_Links_Subdomain, PLL_Links_Domain
565 */
566 $class = apply_filters( 'pll_links_model', $class );
567
568 return new $class( $this );
569 }
570
571 /**
572 * Some backward compatibility with Polylang < 1.8
573 * allows for example to call $polylang->model->get_post_languages( $post_id ) instead of $polylang->model->post->get_language( $post_id )
574 * this works but should be slower than the direct call, thus an error is triggered in debug mode
575 *
576 * @since 1.8
577 *
578 * @param string $func function name
579 * @param array $args function arguments
580 */
581 public function __call( $func, $args ) {
582 $f = $func;
583
584 switch ( $func ) {
585 case 'get_object_term':
586 $o = false === strpos( $args[1], 'term' ) ? 'post' : 'term';
587 break;
588
589 case 'save_translations':
590 case 'delete_translation':
591 case 'get_translations':
592 case 'get_translation':
593 case 'join_clause':
594 $o = ( 'post' == $args[0] || $this->is_translated_post_type( $args[0] ) ) ? 'post' : ( 'term' == $args[0] || $this->is_translated_taxonomy( $args[0] ) ? 'term' : false );
595 unset( $args[0] );
596 break;
597
598 case 'set_post_language':
599 case 'get_post_language':
600 case 'set_term_language':
601 case 'get_term_language':
602 case 'delete_term_language':
603 case 'get_post':
604 case 'get_term':
605 $str = explode( '_', $func );
606 $f = empty( $str[2] ) ? $str[0] : $str[0] . '_' . $str[2];
607 $o = $str[1];
608 break;
609
610 case 'where_clause':
611 case 'get_objects_in_language':
612 $o = $args[1];
613 unset( $args[1] );
614 break;
615 }
616
617 if ( ! empty( $o ) && is_object( $this->$o ) && method_exists( $this->$o, $f ) ) {
618 if ( WP_DEBUG ) {
619 $debug = debug_backtrace();
620 $i = 1 + empty( $debug[1]['line'] ); // the file and line are in $debug[2] if the function was called using call_user_func
621
622 trigger_error( sprintf(
623 '%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",
624 $func, $o, $f, $debug[ $i ]['file'], $debug[ $i ]['line']
625 ) );
626 }
627 return call_user_func_array( array( $this->$o, $f ), $args );
628 }
629
630 $debug = debug_backtrace();
631 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 );
632 }
633 }
634