PluginProbe
Polylang / 2.5.4
Polylang v2.5.4
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.5.4, at include/model.php

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