epend_attachment() * * @param string $p The attachment HTML output. */ $p = apply_filters( 'prepend_attachment', $p ); return "$p\n$content"; } // // Misc. // /** * Retrieves protected post password form content. * * @since 1.0.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string HTML content for password form for password protected post. */ function get_the_password_form( $post = 0 ) { $post = get_post( $post ); $label = 'pwbox-' . ( empty( $post->ID ) ? rand() : $post->ID ); $output = '

' . __( 'This content is password protected. To view it please enter your password below:' ) . '

'; /** * Filters the HTML output for the protected post password form. * * If modifying the password field, please note that the WordPress database schema * limits the password field to 255 characters regardless of the value of the * `minlength` or `maxlength` attributes or other validation that may be added to * the input. * * @since 2.7.0 * @since 5.8.0 Added the `$post` parameter. * * @param string $output The password form HTML output. * @param WP_Post $post Post object. */ return apply_filters( 'the_password_form', $output, $post ); } /** * Determines whether the current post uses a page template. * * This template tag allows you to determine if you are in a page template. * You can optionally provide a template filename or array of template filenames * and then the check will be specific to that template. * * For more information on this and similar theme functions, check out * the {@link https://developer.wordpress.org/themes/basics/conditional-tags/ * Conditional Tags} article in the Theme Developer Handbook. * * @since 2.5.0 * @since 4.2.0 The `$template` parameter was changed to also accept an array of page templates. * @since 4.7.0 Now works with any post type, not just pages. * * @param string|string[] $template The specific template filename or array of templates to match. * @return bool True on success, false on failure. */ function is_page_template( $template = '' ) { if ( ! is_singular() ) { return false; } $page_template = get_page_template_slug( get_queried_object_id() ); if ( empty( $template ) ) { return (bool) $page_template; } if ( $template === $page_template ) { return true; } if ( is_array( $template ) ) { if ( ( in_array( 'default', $template, true ) && ! $page_template ) || in_array( $page_template, $template, true ) ) { return true; } } return ( 'default' === $template && ! $page_template ); } /** * Gets the specific template filename for a given post. * * @since 3.4.0 * @since 4.7.0 Now works with any post type, not just pages. * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @return string|false Page template filename. Returns an empty string when the default page template * is in use. Returns false if the post does not exist. */ function get_page_template_slug( $post = null ) { $post = get_post( $post ); if ( ! $post ) { return false; } $template = get_post_meta( $post->ID, '_wp_page_template', true ); if ( ! $template || 'default' === $template ) { return ''; } return $template; } /** * Retrieves formatted date timestamp of a revision (linked to that revisions's page). * * @since 2.6.0 * * @param int|WP_Post $revision Revision ID or revision object. * @param bool $link Optional. Whether to link to revision's page. Default true. * @return string|false i18n formatted datetimestamp or localized 'Current Revision'. */ function wp_post_revision_title( $revision, $link = true ) { $revision = get_post( $revision ); if ( ! $revision ) { return $revision; } if ( ! in_array( $revision->post_type, array( 'post', 'page', 'revision' ), true ) ) { return false; } /* translators: Revision date format, see https://www.php.net/manual/datetime.format.php */ $datef = _x( 'F j, Y @ H:i:s', 'revision date format' ); /* translators: %s: Revision date. */ $autosavef = __( '%s [Autosave]' ); /* translators: %s: Revision date. */ $currentf = __( '%s [Current Revision]' ); $date = date_i18n( $datef, strtotime( $revision->post_modified ) ); $edit_link = get_edit_post_link( $revision->ID ); if ( $link && current_user_can( 'edit_post', $revision->ID ) && $edit_link ) { $date = "$date"; } if ( ! wp_is_post_revision( $revision ) ) { $date = sprintf( $currentf, $date ); } elseif ( wp_is_post_autosave( $revision ) ) { $date = sprintf( $autosavef, $date ); } return $date; } /** * Retrieves formatted date timestamp of a revision (linked to that revisions's page). * * @since 3.6.0 * * @param int|WP_Post $revision Revision ID or revision object. * @param bool $link Optional. Whether to link to revision's page. Default true. * @return string|false gravatar, user, i18n formatted datetimestamp or localized 'Current Revision'. */ function wp_post_revision_title_expanded( $revision, $link = true ) { $revision = get_post( $revision ); if ( ! $revision ) { return $revision; } if ( ! in_array( $revision->post_type, array( 'post', 'page', 'revision' ), true ) ) { return false; } $author = get_the_author_meta( 'display_name', $revision->post_author ); /* translators: Revision date format, see https://www.php.net/manual/datetime.format.php */ $datef = _x( 'F j, Y @ H:i:s', 'revision date format' ); $gravatar = get_avatar( $revision->post_author, 24 ); $date = date_i18n( $datef, strtotime( $revision->post_modified ) ); $edit_link = get_edit_post_link( $revision->ID ); if ( $link && current_user_can( 'edit_post', $revision->ID ) && $edit_link ) { $date = "$date"; } $revision_date_author = sprintf( /* translators: Post revision title. 1: Author avatar, 2: Author name, 3: Time ago, 4: Date. */ __( '%1$s %2$s, %3$s ago (%4$s)' ), $gravatar, $author, human_time_diff( strtotime( $revision->post_modified_gmt ) ), $date ); /* translators: %s: Revision date with author avatar. */ $autosavef = __( '%s [Autosave]' ); /* translators: %s: Revision date with author avatar. */ $currentf = __( '%s [Current Revision]' ); if ( ! wp_is_post_revision( $revision ) ) { $revision_date_author = sprintf( $currentf, $revision_date_author ); } elseif ( wp_is_post_autosave( $revision ) ) { $revision_date_author = sprintf( $autosavef, $revision_date_author ); } /** * Filters the formatted author and date for a revision. * * @since 4.4.0 * * @param string $revision_date_author The formatted string. * @param WP_Post $revision The revision object. * @param bool $link Whether to link to the revisions page, as passed into * wp_post_revision_title_expanded(). */ return apply_filters( 'wp_post_revision_title_expanded', $revision_date_author, $revision, $link ); } /** * Displays a list of a post's revisions. * * Can output either a UL with edit links or a TABLE with diff interface, and * restore action links. * * @since 2.6.0 * * @param int|WP_Post $post Optional. Post ID or WP_Post object. Default is global $post. * @param string $type 'all' (default), 'revision' or 'autosave' */ function wp_list_post_revisions( $post = 0, $type = 'all' ) { $post = get_post( $post ); if ( ! $post ) { return; } // $args array with (parent, format, right, left, type) deprecated since 3.6. if ( is_array( $type ) ) { $type = ! empty( $type['type'] ) ? $type['type'] : $type; _deprecated_argument( __FUNCTION__, '3.6.0' ); } $revisions = wp_get_post_revisions( $post->ID ); if ( ! $revisions ) { return; } $rows = ''; foreach ( $revisions as $revision ) { if ( ! current_user_can( 'read_post', $revision->ID ) ) { continue; } $is_autosave = wp_is_post_autosave( $revision ); if ( ( 'revision' === $type && $is_autosave ) || ( 'autosave' === $type && ! $is_autosave ) ) { continue; } $rows .= "\t
  • " . wp_post_revision_title_expanded( $revision ) . "
  • \n"; } echo "

    " . __( 'JavaScript must be enabled to use this feature.' ) . "

    \n"; echo "'; } /** * Retrieves the parent post object for the given post. * * @since 5.7.0 * * @param int|WP_Post|null $post Optional. Post ID or WP_Post object. Default is global $post. * @return WP_Post|null Parent post object, or null if there isn't one. */ function get_post_parent( $post = null ) { $wp_post = get_post( $post ); return ! empty( $wp_post->post_parent ) ? get_post( $wp_post->post_parent ) : null; } /** * Returns whether the given post has a parent post. * * @since 5.7.0 * * @param int|WP_Post|null $post Optional. Post ID or WP_Post object. Default is global $post. * @return bool Whether the post has a parent post. */ function has_post_parent( $post = null ) { return (bool) get_post_parent( $post ); } render(); } /** * Parses blocks out of a content string. * * @since 5.0.0 * * @param string $content Post content. * @return array[] { * Array of block structures. * * @type array ...$0 { * An associative array of a single parsed block object. See WP_Block_Parser_Block. * * @type string $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * @type array[] $innerBlocks List of inner blocks. An array of arrays that * have the same structure as this one. * @type string $innerHTML HTML from inside block comment delimiters. * @type array $innerContent List of string fragments and null markers where * inner blocks were found. * } * } */ function parse_blocks( $content ) { /** * Filter to allow plugins to replace the server-side block parser. * * @since 5.0.0 * * @param string $parser_class Name of block parser class. */ $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); $parser = new $parser_class(); return $parser->parse( $content ); } /** * Parses dynamic blocks out of `post_content` and re-renders them. * * @since 5.0.0 * * @param string $content Post content. * @return string Updated post content. */ function do_blocks( $content ) { $blocks = parse_blocks( $content ); $output = ''; foreach ( $blocks as $block ) { $output .= render_block( $block ); } // If there are blocks in this content, we shouldn't run wpautop() on it later. $priority = has_filter( 'the_content', 'wpautop' ); if ( false !== $priority && doing_filter( 'the_content' ) && has_blocks( $content ) ) { remove_filter( 'the_content', 'wpautop', $priority ); add_filter( 'the_content', '_restore_wpautop_hook', $priority + 1 ); } return $output; } /** * If do_blocks() needs to remove wpautop() from the `the_content` filter, this re-adds it afterwards, * for subsequent `the_content` usage. * * @since 5.0.0 * @access private * * @param string $content The post content running through this filter. * @return string The unmodified content. */ function _restore_wpautop_hook( $content ) { $current_priority = has_filter( 'the_content', '_restore_wpautop_hook' ); add_filter( 'the_content', 'wpautop', $current_priority - 1 ); remove_filter( 'the_content', '_restore_wpautop_hook', $current_priority ); return $content; } /** * Returns the current version of the block format that the content string is using. * * If the string doesn't contain blocks, it returns 0. * * @since 5.0.0 * * @param string $content Content to test. * @return int The block format version is 1 if the content contains one or more blocks, 0 otherwise. */ function block_version( $content ) { return has_blocks( $content ) ? 1 : 0; } /** * Registers a new block style. * * @since 5.3.0 * @since 6.6.0 Added support for registering styles for multiple block types. * * @link https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/ * * @param string|string[] $block_name Block type name including namespace or array of namespaced block type names. * @param array $style_properties Array containing the properties of the style name, label, * style_handle (name of the stylesheet to be enqueued), * inline_style (string containing the CSS to be added), * style_data (theme.json-like array to generate CSS from). * See WP_Block_Styles_Registry::register(). * @return bool True if the block style was registered with success and false otherwise. */ function register_block_style( $block_name, $style_properties ) { return WP_Block_Styles_Registry::get_instance()->register( $block_name, $style_properties ); } /** * Unregisters a block style. * * @since 5.3.0 * * @param string $block_name Block type name including namespace. * @param string $block_style_name Block style name. * @return bool True if the block style was unregistered with success and false otherwise. */ function unregister_block_style( $block_name, $block_style_name ) { return WP_Block_Styles_Registry::get_instance()->unregister( $block_name, $block_style_name ); } /** * Checks whether the current block type supports the feature requested. * * @since 5.8.0 * @since 6.4.0 The `$feature` parameter now supports a string. * * @param WP_Block_Type $block_type Block type to check for support. * @param string|array $feature Feature slug, or path to a specific feature to check support for. * @param mixed $default_value Optional. Fallback value for feature support. Default false. * @return bool Whether the feature is supported. */ function block_has_support( $block_type, $feature, $default_value = false ) { $block_support = $default_value; if ( $block_type instanceof WP_Block_Type ) { if ( is_array( $feature ) && count( $feature ) === 1 ) { $feature = $feature[0]; } if ( is_array( $feature ) ) { $block_support = _wp_array_get( $block_type->supports, $feature, $default_value ); } elseif ( isset( $block_type->supports[ $feature ] ) ) { $block_support = $block_type->supports[ $feature ]; } } return true === $block_support || is_array( $block_support ); } /** * Converts typography keys declared under `supports.*` to `supports.typography.*`. * * Displays a `_doing_it_wrong()` notice when a block using the older format is detected. * * @since 5.8.0 * * @param array $metadata Metadata for registering a block type. * @return array Filtered metadata for registering a block type. */ function wp_migrate_old_typography_shape( $metadata ) { if ( ! isset( $metadata['supports'] ) ) { return $metadata; } $typography_keys = array( '__experimentalFontFamily', '__experimentalFontStyle', '__experimentalFontWeight', '__experimentalLetterSpacing', '__experimentalTextDecoration', '__experimentalTextTransform', 'fontSize', 'lineHeight', ); foreach ( $typography_keys as $typography_key ) { $support_for_key = isset( $metadata['supports'][ $typography_key ] ) ? $metadata['supports'][ $typography_key ] : null; if ( null !== $support_for_key ) { _doing_it_wrong( 'register_block_type_from_metadata()', sprintf( /* translators: 1: Block type, 2: Typography supports key, e.g: fontSize, lineHeight, etc. 3: block.json, 4: Old metadata key, 5: New metadata key. */ __( 'Block "%1$s" is declaring %2$s support in %3$s file under %4$s. %2$s support is now declared under %5$s.' ), $metadata['name'], "$typography_key", 'block.json', "supports.$typography_key", "supports.typography.$typography_key" ), '5.8.0' ); _wp_array_set( $metadata['supports'], array( 'typography', $typography_key ), $support_for_key ); unset( $metadata['supports'][ $typography_key ] ); } } return $metadata; } /** * Helper function that constructs a WP_Query args array from * a `Query` block properties. * * It's used in Query Loop, Query Pagination Numbers and Query Pagination Next blocks. * * @since 5.8.0 * @since 6.1.0 Added `query_loop_block_query_vars` filter and `parents` support in query. * @since 6.7.0 Added support for the `format` property in query. * * @param WP_Block $block Block instance. * @param int $page Current query's page. * * @return array Returns the constructed WP_Query arguments. */ function build_query_vars_from_query_block( $block, $page ) { $query = array( 'post_type' => 'post', 'order' => 'DESC', 'orderby' => 'date', 'post__not_in' => array(), 'tax_query' => array(), ); if ( isset( $block->context['query'] ) ) { if ( ! empty( $block->context['query']['postType'] ) ) { $post_type_param = $block->context['query']['postType']; if ( is_post_type_viewable( $post_type_param ) ) { $query['post_type'] = $post_type_param; } } if ( isset( $block->context['query']['sticky'] ) && ! empty( $block->context['query']['sticky'] ) ) { $sticky = get_option( 'sticky_posts' ); if ( 'only' === $block->context['query']['sticky'] ) { /* * Passing an empty array to post__in will return have_posts() as true (and all posts will be returned). * Logic should be used before hand to determine if WP_Query should be used in the event that the array * being passed to post__in is empty. * * @see https://core.trac.wordpress.org/ticket/28099 */ $query['post__in'] = ! empty( $sticky ) ? $sticky : array( 0 ); $query['ignore_sticky_posts'] = 1; } else { $query['post__not_in'] = array_merge( $query['post__not_in'], $sticky ); } } if ( ! empty( $block->context['query']['exclude'] ) ) { $excluded_post_ids = array_map( 'intval', $block->context['query']['exclude'] ); $excluded_post_ids = array_filter( $excluded_post_ids ); $query['post__not_in'] = array_merge( $query['post__not_in'], $excluded_post_ids ); } if ( isset( $block->context['query']['perPage'] ) && is_numeric( $block->context['query']['perPage'] ) ) { $per_page = absint( $block->context['query']['perPage'] ); $offset = 0; if ( isset( $block->context['query']['offset'] ) && is_numeric( $block->context['query']['offset'] ) ) { $offset = absint( $block->context['query']['offset'] ); } $query['offset'] = ( $per_page * ( $page - 1 ) ) + $offset; $query['posts_per_page'] = $per_page; } // Migrate `categoryIds` and `tagIds` to `tax_query` for backwards compatibility. if ( ! empty( $block->context['query']['categoryIds'] ) || ! empty( $block->context['query']['tagIds'] ) ) { $tax_query_back_compat = array(); if ( ! empty( $block->context['query']['categoryIds'] ) ) { $tax_query_back_compat[] = array( 'taxonomy' => 'category', 'terms' => array_filter( array_map( 'intval', $block->context['query']['categoryIds'] ) ), 'include_children' => false, ); } if ( ! empty( $block->context['query']['tagIds'] ) ) { $tax_query_back_compat[] = array( 'taxonomy' => 'post_tag', 'terms' => array_filter( array_map( 'intval', $block->context['query']['tagIds'] ) ), 'include_children' => false, ); } $query['tax_query'] = array_merge( $query['tax_query'], $tax_query_back_compat ); } if ( ! empty( $block->context['query']['taxQuery'] ) ) { $tax_query = array(); foreach ( $block->context['query']['taxQuery'] as $taxonomy => $terms ) { if ( is_taxonomy_viewable( $taxonomy ) && ! empty( $terms ) ) { $tax_query[] = array( 'taxonomy' => $taxonomy, 'terms' => array_filter( array_map( 'intval', $terms ) ), 'include_children' => false, ); } } $query['tax_query'] = array_merge( $query['tax_query'], $tax_query ); } if ( ! empty( $block->context['query']['format'] ) && is_array( $block->context['query']['format'] ) ) { $formats = $block->context['query']['format']; /* * Validate that the format is either `standard` or a supported post format. * - First, add `standard` to the array of valid formats. * - Then, remove any invalid formats. */ $valid_formats = array_merge( array( 'standard' ), get_post_format_slugs() ); $formats = array_intersect( $formats, $valid_formats ); /* * The relation needs to be set to `OR` since the request can contain * two separate conditions. The user may be querying for items that have * either the `standard` format or a specific format. */ $formats_query = array( 'relation' => 'OR' ); /* * The default post format, `standard`, is not stored in the database. * If `standard` is part of the request, the query needs to exclude all post items that * have a format assigned. */ if ( in_array( 'standard', $formats, true ) ) { $formats_query[] = array( 'taxonomy' => 'post_format', 'field' => 'slug', 'operator' => 'NOT EXISTS', ); // Remove the `standard` format, since it cannot be queried. unset( $formats[ array_search( 'standard', $formats, true ) ] ); } // Add any remaining formats to the formats query. if ( ! empty( $formats ) ) { // Add the `post-format-` prefix. $terms = array_map( static function ( $format ) { return "post-format-$format"; }, $formats ); $formats_query[] = array( 'taxonomy' => 'post_format', 'field' => 'slug', 'terms' => $terms, 'operator' => 'IN', ); } /* * Add `$formats_query` to `$query`, as long as it contains more than one key: * If `$formats_query` only contains the initial `relation` key, there are no valid formats to query, * and the query should not be modified. */ if ( count( $formats_query ) > 1 ) { // Enable filtering by both post formats and other taxonomies by combining them with `AND`. if ( empty( $query['tax_query'] ) ) { $query['tax_query'] = $formats_query; } else { $query['tax_query'] = array( 'relation' => 'AND', $query['tax_query'], $formats_query, ); } } } if ( isset( $block->context['query']['order'] ) && in_array( strtoupper( $block->context['query']['order'] ), array( 'ASC', 'DESC' ), true ) ) { $query['order'] = strtoupper( $block->context['query']['order'] ); } if ( isset( $block->context['query']['orderBy'] ) ) { $query['orderby'] = $block->context['query']['orderBy']; } if ( isset( $block->context['query']['author'] ) ) { if ( is_array( $block->context['query']['author'] ) ) { $query['author__in'] = array_filter( array_map( 'intval', $block->context['query']['author'] ) ); } elseif ( is_string( $block->context['query']['author'] ) ) { $query['author__in'] = array_filter( array_map( 'intval', explode( ',', $block->context['query']['author'] ) ) ); } elseif ( is_int( $block->context['query']['author'] ) && $block->context['query']['author'] > 0 ) { $query['author'] = $block->context['query']['author']; } } if ( ! empty( $block->context['query']['search'] ) ) { $query['s'] = $block->context['query']['search']; } if ( ! empty( $block->context['query']['parents'] ) && is_post_type_hierarchical( $query['post_type'] ) ) { $query['post_parent__in'] = array_filter( array_map( 'intval', $block->context['query']['parents'] ) ); } } /** * Filters the arguments which will be passed to `WP_Query` for the Query Loop Block. * * Anything to this filter should be compatible with the `WP_Query` API to form * the query context which will be passed down to the Query Loop Block's children. * This can help, for example, to include additional settings or meta queries not * directly supported by the core Query Loop Block, and extend its capabilities. * * Please note that this will only influence the query that will be rendered on the * front-end. The editor preview is not affected by this filter. Also, worth noting * that the editor preview uses the REST API, so, ideally, one should aim to provide * attributes which are also compatible with the REST API, in order to be able to * implement identical queries on both sides. * * @since 6.1.0 * * @param array $query Array containing parameters for `WP_Query` as parsed by the block context. * @param WP_Block $block Block instance. * @param int $page Current query's page. */ return apply_filters( 'query_loop_block_query_vars', $query, $block, $page ); } /** * Helper function that returns the proper pagination arrow HTML for * `QueryPaginationNext` and `QueryPaginationPrevious` blocks based * on the provided `paginationArrow` from `QueryPagination` context. * * It's used in QueryPaginationNext and QueryPaginationPrevious blocks. * * @since 5.9.0 * * @param WP_Block $block Block instance. * @param bool $is_next Flag for handling `next/previous` blocks. * @return string|null The pagination arrow HTML or null if there is none. */ function get_query_pagination_arrow( $block, $is_next ) { $arrow_map = array( 'none' => '', 'arrow' => array( 'next' => '→', 'previous' => '←', ), 'chevron' => array( 'next' => '»', 'previous' => '«', ), ); if ( ! empty( $block->context['paginationArrow'] ) && array_key_exists( $block->context['paginationArrow'], $arrow_map ) && ! empty( $arrow_map[ $block->context['paginationArrow'] ] ) ) { $pagination_type = $is_next ? 'next' : 'previous'; $arrow_attribute = $block->context['paginationArrow']; $arrow = $arrow_map[ $block->context['paginationArrow'] ][ $pagination_type ]; $arrow_classes = "wp-block-query-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; return ""; } return null; } /** * Helper function that constructs a comment query vars array from the passed * block properties. * * It's used with the Comment Query Loop inner blocks. * * @since 6.0.0 * * @param WP_Block $block Block instance. * @return array Returns the comment query parameters to use with the * WP_Comment_Query constructor. */ function build_comment_query_vars_from_block( $block ) { $comment_args = array( 'orderby' => 'comment_date_gmt', 'order' => 'ASC', 'status' => 'approve', 'no_found_rows' => false, ); if ( is_user_logged_in() ) { $comment_args['include_unapproved'] = array( get_current_user_id() ); } else { $unapproved_email = wp_get_unapproved_comment_author_email(); if ( $unapproved_email ) { $comment_args['include_unapproved'] = array( $unapproved_email ); } } if ( ! empty( $block->context['postId'] ) ) { $comment_args['post_id'] = (int) $block->context['postId']; } if ( get_option( 'thread_comments' ) ) { $comment_args['hierarchical'] = 'threaded'; } else { $comment_args['hierarchical'] = false; } if ( get_option( 'page_comments' ) === '1' || get_option( 'page_comments' ) === true ) { $per_page = get_option( 'comments_per_page' ); $default_page = get_option( 'default_comments_page' ); if ( $per_page > 0 ) { $comment_args['number'] = $per_page; $page = (int) get_query_var( 'cpage' ); if ( $page ) { $comment_args['paged'] = $page; } elseif ( 'oldest' === $default_page ) { $comment_args['paged'] = 1; } elseif ( 'newest' === $default_page ) { $max_num_pages = (int) ( new WP_Comment_Query( $comment_args ) )->max_num_pages; if ( 0 !== $max_num_pages ) { $comment_args['paged'] = $max_num_pages; } } } } return $comment_args; } /** * Helper function that returns the proper pagination arrow HTML for * `CommentsPaginationNext` and `CommentsPaginationPrevious` blocks based on the * provided `paginationArrow` from `CommentsPagination` context. * * It's used in CommentsPaginationNext and CommentsPaginationPrevious blocks. * * @since 6.0.0 * * @param WP_Block $block Block instance. * @param string $pagination_type Optional. Type of the arrow we will be rendering. * Accepts 'next' or 'previous'. Default 'next'. * @return string|null The pagination arrow HTML or null if there is none. */ function get_comments_pagination_arrow( $block, $pagination_type = 'next' ) { $arrow_map = array( 'none' => '', 'arrow' => array( 'next' => '→', 'previous' => '←', ), 'chevron' => array( 'next' => '»', 'previous' => '«', ), ); if ( ! empty( $block->context['comments/paginationArrow'] ) && ! empty( $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ] ) ) { $arrow_attribute = $block->context['comments/paginationArrow']; $arrow = $arrow_map[ $block->context['comments/paginationArrow'] ][ $pagination_type ]; $arrow_classes = "wp-block-comments-pagination-$pagination_type-arrow is-arrow-$arrow_attribute"; return ""; } return null; } /** * Strips all HTML from the content of footnotes, and sanitizes the ID. * * This function expects slashed data on the footnotes content. * * @access private * @since 6.3.2 * * @param string $footnotes JSON-encoded string of an array containing the content and ID of each footnote. * @return string Filtered content without any HTML on the footnote content and with the sanitized ID. */ function _wp_filter_post_meta_footnotes( $footnotes ) { $footnotes_decoded = json_decode( $footnotes, true ); if ( ! is_array( $footnotes_decoded ) ) { return ''; } $footnotes_sanitized = array(); foreach ( $footnotes_decoded as $footnote ) { if ( ! empty( $footnote['content'] ) && ! empty( $footnote['id'] ) ) { $footnotes_sanitized[] = array( 'id' => sanitize_key( $footnote['id'] ), 'content' => wp_unslash( wp_filter_post_kses( wp_slash( $footnote['content'] ) ) ), ); } } return wp_json_encode( $footnotes_sanitized ); } /** * Adds the filters for footnotes meta field. * * @access private * @since 6.3.2 */ function _wp_footnotes_kses_init_filters() { add_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); } /** * Removes the filters for footnotes meta field. * * @access private * @since 6.3.2 */ function _wp_footnotes_remove_filters() { remove_filter( 'sanitize_post_meta_footnotes', '_wp_filter_post_meta_footnotes' ); } /** * Registers the filter of footnotes meta field if the user does not have `unfiltered_html` capability. * * @access private * @since 6.3.2 */ function _wp_footnotes_kses_init() { _wp_footnotes_remove_filters(); if ( ! current_user_can( 'unfiltered_html' ) ) { _wp_footnotes_kses_init_filters(); } } /** * Initializes the filters for footnotes meta field when imported data should be filtered. * * This filter is the last one being executed on {@see 'force_filtered_html_on_import'}. * If the input of the filter is true, it means we are in an import situation and should * enable kses, independently of the user capabilities. So in that case we call * _wp_footnotes_kses_init_filters(). * * @access private * @since 6.3.2 * * @param string $arg Input argument of the filter. * @return string Input argument of the filter. */ function _wp_footnotes_force_filtered_html_on_import_filter( $arg ) { // If `force_filtered_html_on_import` is true, we need to init the global styles kses filters. if ( $arg ) { _wp_footnotes_kses_init_filters(); } return $arg; } $value = $caster instanceof CastsInboundAttributes ? $value : $caster->get($this, $key, $value, $this->attributes); if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; } return $value; } } /** * Cast the given attribute to an enum. * * @param string $key * @param mixed $value * @return mixed */ protected function getEnumCastableAttributeValue($key, $value) { if (is_null($value)) { return; } $castType = $this->getCasts()[$key]; if ($value instanceof $castType) { return $value; } return $castType::from($value); } /** * Get the type of cast for a model attribute. * * @param string $key * @return string */ protected function getCastType($key) { if ($this->isCustomDateTimeCast($this->getCasts()[$key])) { return 'custom_datetime'; } if ($this->isImmutableCustomDateTimeCast($this->getCasts()[$key])) { return 'immutable_custom_datetime'; } if ($this->isDecimalCast($this->getCasts()[$key])) { return 'decimal'; } return trim(strtolower($this->getCasts()[$key])); } /** * Increment or decrement the given attribute using the custom cast class. * * @param string $method * @param string $key * @param mixed $value * @return mixed */ protected function deviateClassCastableAttribute($method, $key, $value) { return $this->resolveCasterClass($key)->{$method}( $this, $key, $value, $this->attributes ); } /** * Serialize the given attribute using the custom cast class. * * @param string $key * @param mixed $value * @return mixed */ protected function serializeClassCastableAttribute($key, $value) { return $this->resolveCasterClass($key)->serialize( $this, $key, $value, $this->attributes ); } /** * Determine if the cast type is a custom date time cast. * * @param string $cast * @return bool */ protected function isCustomDateTimeCast($cast) { return strncmp($cast, 'date:', 5) === 0 || strncmp($cast, 'datetime:', 9) === 0; } /** * Determine if the cast type is an immutable custom date time cast. * * @param string $cast * @return bool */ protected function isImmutableCustomDateTimeCast($cast) { return strncmp($cast, 'immutable_date:', 15) === 0 || strncmp($cast, 'immutable_datetime:', 19) === 0; } /** * Determine if the cast type is a decimal cast. * * @param string $cast * @return bool */ protected function isDecimalCast($cast) { return strncmp($cast, 'decimal:', 8) === 0; } /** * Set a given attribute on the model. * * @param string $key * @param mixed $value * @return mixed */ public function setAttribute($key, $value) { // First we will check for the presence of a mutator for the set operation // which simply lets the developers tweak the attribute as it is set on // this model, such as "json_encoding" a listing of data for storage. if ($this->hasSetMutator($key)) { return $this->setMutatedAttributeValue($key, $value); } elseif ($this->hasAttributeSetMutator($key)) { return $this->setAttributeMarkedMutatedAttributeValue($key, $value); } // If an attribute is listed as a "date", we'll convert it from a DateTime // instance into a form proper for storage on the database tables using // the connection grammar's date format. We will auto set the values. elseif ($value && $this->isDateAttribute($key)) { $value = $this->fromDateTime($value); } if ($this->isEnumCastable($key)) { $this->setEnumCastableAttribute($key, $value); return $this; } if ($this->isClassCastable($key)) { $this->setClassCastableAttribute($key, $value); return $this; } if (! is_null($value) && $this->isJsonCastable($key)) { $value = $this->castAttributeAsJson($key, $value); } // If this attribute contains a JSON ->, we'll set the proper value in the // attribute's underlying array. This takes care of properly nesting an // attribute in the array's value in the case of deeply nested items. if (Str::contains($key, '->')) { return $this->fillJsonAttribute($key, $value); } if (! is_null($value) && $this->isEncryptedCastable($key)) { $value = $this->castAttributeAsEncryptedString($key, $value); } $this->attributes[$key] = $value; return $this; } /** * Determine if a set mutator exists for an attribute. * * @param string $key * @return bool */ public function hasSetMutator($key) { return method_exists($this, 'set'.Str::studly($key).'Attribute'); } /** * Determine if an "Attribute" return type marked set mutator exists for an attribute. * * @param string $key * @return bool */ public function hasAttributeSetMutator($key) { $class = get_class($this); if (isset(static::$setAttributeMutatorCache[$class][$key])) { return static::$setAttributeMutatorCache[$class][$key]; } if (! method_exists($this, $method = Str::camel($key))) { return static::$setAttributeMutatorCache[$class][$key] = false; } $returnType = (new ReflectionMethod($this, $method))->getReturnType(); return static::$setAttributeMutatorCache[$class][$key] = $returnType && $returnType instanceof ReflectionNamedType && $returnType->getName() === Attribute::class && is_callable($this->{$method}()->set); } /** * Set the value of an attribute using its mutator. * * @param string $key * @param mixed $value * @return mixed */ protected function setMutatedAttributeValue($key, $value) { return $this->{'set'.Str::studly($key).'Attribute'}($value); } /** * Set the value of a "Attribute" return type marked attribute using its mutator. * * @param string $key * @param mixed $value * @return mixed */ protected function setAttributeMarkedMutatedAttributeValue($key, $value) { $attribute = $this->{Str::camel($key)}(); $callback = $attribute->set ?: function ($value) use ($key) { $this->attributes[$key] = $value; }; $this->attributes = array_merge( $this->attributes, $this->normalizeCastClassResponse( $key, call_user_func($callback, $value, $this->attributes) ) ); if (! is_object($value) || ! $attribute->withObjectCaching) { unset($this->attributeCastCache[$key]); } else { $this->attributeCastCache[$key] = $value; } } /** * Determine if the given attribute is a date or date castable. * * @param string $key * @return bool */ protected function isDateAttribute($key) { return in_array($key, $this->getDates(), true) || $this->isDateCastable($key); } /** * Set a given JSON attribute on the model. * * @param string $key * @param mixed $value * @return $this */ public function fillJsonAttribute($key, $value) { [$key, $path] = explode('->', $key, 2); $value = $this->asJson($this->getArrayAttributeWithValue( $path, $key, $value )); $this->attributes[$key] = $this->isEncryptedCastable($key) ? $this->castAttributeAsEncryptedString($key, $value) : $value; return $this; } /** * Set the value of a class castable attribute. * * @param string $key * @param mixed $value * @return void */ protected function setClassCastableAttribute($key, $value) { $caster = $this->resolveCasterClass($key); if (is_null($value)) { $this->attributes = array_merge($this->attributes, array_map( function () { }, $this->normalizeCastClassResponse($key, $caster->set( $this, $key, $this->{$key}, $this->attributes )) )); } else { $this->attributes = array_merge( $this->attributes, $this->normalizeCastClassResponse($key, $caster->set( $this, $key, $value, $this->attributes )) ); } if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; } } /** * Set the value of an enum castable attribute. * * @param string $key * @param \BackedEnum $value * @return void */ protected function setEnumCastableAttribute($key, $value) { $enumClass = $this->getCasts()[$key]; if (! isset($value)) { $this->attributes[$key] = null; } elseif ($value instanceof $enumClass) { $this->attributes[$key] = $value->value; } else { $this->attributes[$key] = $enumClass::from($value)->value; } } /** * Get an array attribute with the given key and value set. * * @param string $path * @param string $key * @param mixed $value * @return $this */ protected function getArrayAttributeWithValue($path, $key, $value) { return Helper::tap($this->getArrayAttributeByKey($key), function (&$array) use ($path, $value) { Arr::set($array, str_replace('->', '.', $path), $value); }); } /** * Get an array attribute or return an empty array if it is not set. * * @param string $key * @return array */ protected function getArrayAttributeByKey($key) { if (! isset($this->attributes[$key])) { return []; } return $this->fromJson( $this->isEncryptedCastable($key) ? $this->fromEncryptedString($this->attributes[$key]) : $this->attributes[$key] ); } /** * Cast the given attribute to JSON. * * @param string $key * @param mixed $value * @return string */ protected function castAttributeAsJson($key, $value) { $value = $this->asJson($value); if ($value === false) { throw JsonEncodingException::forAttribute( $this, $key, json_last_error_msg() ); } return $value; } /** * Encode the given value as JSON. * * @param mixed $value * @return string */ protected function asJson($value) { return json_encode($value); } /** * Decode the given JSON back into an array or object. * * @param string $value * @param bool $asObject * @return mixed */ public function fromJson($value, $asObject = false) { return json_decode($value, ! $asObject); } /** * Decrypt the given encrypted string. * * @param string $value * @return mixed */ public function fromEncryptedString($value) { return (static::$encrypter ?? App::make('encrypter'))->decrypt($value, false); } /** * Cast the given attribute to an encrypted string. * * @param string $key * @param mixed $value * @return string */ protected function castAttributeAsEncryptedString($key, $value) { return (static::$encrypter ?? App::make('encrypter'))->encrypt($value, false); } /** * Set the encrypter instance that will be used to encrypt attributes. * * @param \NinjaTables\Framework\Encryption\Encrypter $encrypter * @return void */ public static function encryptUsing($encrypter) { static::$encrypter = $encrypter; } /** * Decode the given float. * * @param mixed $value * @return mixed */ public function fromFloat($value) { switch ((string) $value) { case 'Infinity': return INF; case '-Infinity': return -INF; case 'NaN': return NAN; default: return (float) $value; } } /** * Return a decimal as string. * * @param float $value * @param int $decimals * @return string */ protected function asDecimal($value, $decimals) { return number_format($value, $decimals, '.', ''); } /** * Return a timestamp as DateTime object with time set to 00:00:00. * * @param mixed $value * @return \NinjaTables\Framework\Support\DateTime; */ protected function asDate($value) { return $this->asDateTime($value)->setTime(0, 0, 0, 0); } /** * Return a timestamp as DateTime object. * * @param mixed $value * @return \NinjaTables\Framework\Support\DateTime; */ protected function asDateTime($value) { // If this value is already a DateTime instance, we shall just return it as is. if ($value instanceof DateTime) { return $value; } // If the value is already a DateTime instance, we will just skip the rest of // these checks since they will be a waste of time, and hinder performance // when checking the field. We will just return the DateTime right away. if ($value instanceof DateTimeInterface) { return new DateTime( $value->format('Y-m-d H:i:s.u'), $value->getTimeZone() ); } // If this value is an integer, we will assume it is a UNIX timestamp's value // and format a DateTime object from this timestamp. This allows flexibility // when defining your date fields as they might be UNIX timestamps here. if (is_numeric($value)) { $dateTime = new DateTime(); $dateTime->setTimestamp($value); return $dateTime; } // If the value is in simply year, month, day format, we will instantiate the // DateTime instances from that format. Again, this provides for simple date // fields on the database. if (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value)) { $dateTime = DateTime::createFromFormat('Y-m-d', $value); $dateTime->setTime(0, 0, 0); return $dateTime; } // Finally, we will just assume this date is in the format used by default on // the database connection and use that format to create the DateTime object // that is returned back out to the developers after we convert it here. return DateTime::createFromFormat($this->getDateFormat(), $value); } /** * Determine if the given value is a standard date format. * * @param string $value * @return bool */ protected function isStandardDateFormat($value) { return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value); } /** * Convert a DateTime to a storable string. * * @param mixed $value * @return string|null */ public function fromDateTime($value) { return empty($value) ? $value : $this->asDateTime($value)->format( $this->getDateFormat() ); } /** * Return a timestamp as unix timestamp. * * @param mixed $value * @return int */ protected function asTimestamp($value) { return $this->asDateTime($value)->getTimestamp(); } /** * Prepare a date for array / JSON serialization. * * @param \DateTimeInterface $date * @return string */ protected function serializeDate(DateTimeInterface $date) { return $date->toJSON(); } /** * Get the attributes that should be converted to dates. * * @return array */ public function getDates() { if (! $this->usesTimestamps()) { return $this->dates; } $defaults = [ $this->getCreatedAtColumn(), $this->getUpdatedAtColumn(), ]; return array_unique(array_merge($this->dates, $defaults)); } /** * Get the format for database stored dates. * * @return string */ public function getDateFormat() { return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat(); } /** * Set the date format used by the model. * * @param string $format * @return $this */ public function setDateFormat($format) { $this->dateFormat = $format; return $this; } /** * Determine whether an attribute should be cast to a native type. * * @param string $key * @param array|string|null $types * @return bool */ public function hasCast($key, $types = null) { if (array_key_exists($key, $this->getCasts())) { return $types ? in_array($this->getCastType($key), (array) $types, true) : true; } return false; } /** * Get the casts array. * * @return array */ public function getCasts() { if ($this->getIncrementing()) { return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts); } return $this->casts; } /** * Determine whether a value is Date / DateTime castable for inbound manipulation. * * @param string $key * @return bool */ protected function isDateCastable($key) { return $this->hasCast($key, ['date', 'datetime', 'immutable_date', 'immutable_datetime']); } /** * Determine whether a value is Date / DateTime custom-castable for inbound manipulation. * * @param string $key * @return bool */ protected function isDateCastableWithCustomFormat($key) { return $this->hasCast($key, ['custom_datetime', 'immutable_custom_datetime']); } /** * Determine whether a value is JSON castable for inbound manipulation. * * @param string $key * @return bool */ protected function isJsonCastable($key) { return $this->hasCast($key, ['array', 'json', 'object', 'collection', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); } /** * Determine whether a value is an encrypted castable for inbound manipulation. * * @param string $key * @return bool */ protected function isEncryptedCastable($key) { return $this->hasCast($key, ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object']); } /** * Determine if the given key is cast using a custom class. * * @param string $key * @return bool * * @throws \NinjaTables\Framework\Database\Orm\InvalidCastException */ protected function isClassCastable($key) { if (! array_key_exists($key, $this->getCasts())) { return false; } $castType = $this->parseCasterClass($this->getCasts()[$key]); if (in_array($castType, static::$primitiveCastTypes)) { return false; } if (class_exists($castType)) { return true; } throw new InvalidCastException($this->getModel(), $key, $castType); } /** * Determine if the given key is cast using an enum. * * @param string $key * @return bool */ protected function isEnumCastable($key) { if (! array_key_exists($key, $this->getCasts())) { return false; } $castType = $this->getCasts()[$key]; if (in_array($castType, static::$primitiveCastTypes)) { return false; } if (function_exists('enum_exists') && enum_exists($castType)) { return true; } } /** * Determine if the key is deviable using a custom class. * * @param string $key * @return bool * * @throws \NinjaTables\Framework\Database\Orm\InvalidCastException */ protected function isClassDeviable($key) { return $this->isClassCastable($key) && method_exists($castType = $this->parseCasterClass($this->getCasts()[$key]), 'increment') && method_exists($castType, 'decrement'); } /** * Determine if the key is serializable using a custom class. * * @param string $key * @return bool * * @throws \NinjaTables\Framework\Database\Orm\InvalidCastException */ protected function isClassSerializable($key) { return ! $this->isEnumCastable($key) && $this->isClassCastable($key) && method_exists($this->resolveCasterClass($key), 'serialize'); } /** * Resolve the custom caster class for a given key. * * @param string $key * @return mixed */ protected function resolveCasterClass($key) { $castType = $this->getCasts()[$key]; $arguments = []; if (is_string($castType) && strpos($castType, ':') !== false) { $segments = explode(':', $castType, 2); $castType = $segments[0]; $arguments = explode(',', $segments[1]); } if (is_subclass_of($castType, Castable::class)) { $castType = $castType::castUsing($arguments); } if (is_object($castType)) { return $castType; } return new $castType(...$arguments); } /** * Parse the given caster class, removing any arguments. * * @param string $class * @return string */ protected function parseCasterClass($class) { return strpos($class, ':') === false ? $class : explode(':', $class, 2)[0]; } /** * Merge the cast class and attribute cast attributes back into the model. * * @return void */ protected function mergeAttributesFromCachedCasts() { $this->mergeAttributesFromClassCasts(); $this->mergeAttributesFromAttributeCasts(); } /** * Merge the cast class attributes back into the model. * * @return void */ protected function mergeAttributesFromClassCasts() { foreach ($this->classCastCache as $key => $value) { $caster = $this->resolveCasterClass($key); $this->attributes = array_merge( $this->attributes, $caster instanceof CastsInboundAttributes ? [$key => $value] : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) ); } } /** * Merge the cast class attributes back into the model. * * @return void */ protected function mergeAttributesFromAttributeCasts() { foreach ($this->attributeCastCache as $key => $value) { $attribute = $this->{Str::camel($key)}(); if ($attribute->get && ! $attribute->set) { continue; } $callback = $attribute->set ?: function ($value) use ($key) { $this->attributes[$key] = $value; }; $this->attributes = array_merge( $this->attributes, $this->normalizeCastClassResponse( $key, call_user_func($callback, $value, $this->attributes) ) ); } } /** * Normalize the response from a custom class caster. * * @param string $key * @param mixed $value * @return array */ protected function normalizeCastClassResponse($key, $value) { return is_array($value) ? $value : [$key => $value]; } /** * Get all of the current attributes on the model. * * @return array */ public function getAttributes() { $this->mergeAttributesFromCachedCasts(); return $this->attributes; } /** * Get all of the current attributes on the model for an insert operation. * * @return array */ protected function getAttributesForInsert() { return $this->getAttributes(); } /** * Set the array of model attributes. No checking is done. * * @param array $attributes * @param bool $sync * @return $this */ public function setRawAttributes(array $attributes, $sync = false) { $this->attributes = $attributes; if ($sync) { $this->syncOriginal(); } $this->classCastCache = []; $this->attributeCastCache = []; return $this; } /** * Get the model's original attribute values. * * @param string|null $key * @param mixed $default * @return mixed|array */ public function getOriginal($key = null, $default = null) { return (new static)->setRawAttributes( $this->original, $sync = true )->getOriginalWithoutRewindingModel($key, $default); } /** * Get the model's original attribute values. * * @param string|null $key * @param mixed $default * @return mixed|array */ protected function getOriginalWithoutRewindingModel($key = null, $default = null) { if ($key) { return $this->transformModelValue( $key, Arr::get($this->original, $key, $default) ); } return Helper::collect($this->original)->mapWithKeys(function ($value, $key) { return [$key => $this->transformModelValue($key, $value)]; })->all(); } /** * Get the model's raw original attribute values. * * @param string|null $key * @param mixed $default * @return mixed|array */ public function getRawOriginal($key = null, $default = null) { return Arr::get($this->original, $key, $default); } /** * Get a subset of the model's attributes. * * @param array|mixed $attributes * @return array */ public function only($attributes) { $results = []; foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) { $results[$attribute] = $this->getAttribute($attribute); } return $results; } /** * Sync the original attributes with the current. * * @return $this */ public function syncOriginal() { $this->original = $this->getAttributes(); return $this; } /** * Sync a single original attribute with its current value. * * @param string $attribute * @return $this */ public function syncOriginalAttribute($attribute) { return $this->syncOriginalAttributes($attribute); } /** * Sync multiple original attribute with their current values. * * @param array|string $attributes * @return $this */ public function syncOriginalAttributes($attributes) { $attributes = is_array($attributes) ? $attributes : func_get_args(); $modelAttributes = $this->getAttributes(); foreach ($attributes as $attribute) { $this->original[$attribute] = $modelAttributes[$attribute]; } return $this; } /** * Sync the changed attributes. * * @return $this */ public function syncChanges() { $this->changes = $this->getDirty(); return $this; } /** * Determine if the model or any of the given attribute(s) have been modified. * * @param array|string|null $attributes * @return bool */ public function isDirty($attributes = null) { return $this->hasChanges( $this->getDirty(), is_array($attributes) ? $attributes : func_get_args() ); } /** * Determine if the model or all the given attribute(s) have remained the same. * * @param array|string|null $attributes * @return bool */ public function isClean($attributes = null) { return ! $this->isDirty(...func_get_args()); } /** * Determine if the model or any of the given attribute(s) have been modified. * * @param array|string|null $attributes * @return bool */ public function wasChanged($attributes = null) { return $this->hasChanges( $this->getChanges(), is_array($attributes) ? $attributes : func_get_args() ); } /** * Determine if any of the given attributes were changed. * * @param array $changes * @param array|string|null $attributes * @return bool */ protected function hasChanges($changes, $attributes = null) { // If no specific attributes were provided, we will just see if the dirty array // already contains any attributes. If it does we will just return that this // count is greater than zero. Else, we need to check specific attributes. if (empty($attributes)) { return count($changes) > 0; } // Here we will spin through every attribute and see if this is in the array of // dirty attributes. If it is, we will return true and if we make it through // all of the attributes for the entire array we will return false at end. foreach (Arr::wrap($attributes) as $attribute) { if (array_key_exists($attribute, $changes)) { return true; } } return false; } /** * Get the attributes that have been changed since the last sync. * * @return array */ public function getDirty() { $dirty = []; foreach ($this->getAttributes() as $key => $value) { if (! $this->originalIsEquivalent($key)) { $dirty[$key] = $value; } } return $dirty; } /** * Get the attributes that were changed. * * @return array */ public function getChanges() { return $this->changes; } /** * Determine if the new and old values for a given key are equivalent. * * @param string $key * @return bool */ public function originalIsEquivalent($key) { if (! array_key_exists($key, $this->original)) { return false; } $attribute = Arr::get($this->attributes, $key); $original = Arr::get($this->original, $key); if ($attribute === $original) { return true; } elseif (is_null($attribute)) { return false; } elseif ($this->isDateAttribute($key) || $this->isDateCastableWithCustomFormat($key)) { return $this->fromDateTime($attribute) === $this->fromDateTime($original); } elseif ($this->hasCast($key, ['object', 'collection'])) { return $this->fromJson($attribute) === $this->fromJson($original); } elseif ($this->hasCast($key, ['real', 'float', 'double'])) { if (($attribute === null && $original !== null) || ($attribute !== null && $original === null)) { return false; } return abs($this->castAttribute($key, $attribute) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4; } elseif ($this->hasCast($key, static::$primitiveCastTypes)) { return $this->castAttribute($key, $attribute) === $this->castAttribute($key, $original); } elseif ($this->isClassCastable($key) && in_array($this->getCasts()[$key], [AsArrayObject::class, AsCollection::class])) { return $this->fromJson($attribute) === $this->fromJson($original); } return is_numeric($attribute) && is_numeric($original) && strcmp((string) $attribute, (string) $original) === 0; } /** * Transform a raw model value using mutators, casts, etc. * * @param string $key * @param mixed $value * @return mixed */ protected function transformModelValue($key, $value) { // If the attribute has a get mutator, we will call that then return what // it returns as the value, which is useful for transforming values on // retrieval from the model to a form that is more useful for usage. if ($this->hasGetMutator($key)) { return $this->mutateAttribute($key, $value); } elseif ($this->hasAttributeGetMutator($key)) { return $this->mutateAttributeMarkedAttribute($key, $value); } // If the attribute exists within the cast array, we will convert it to // an appropriate native PHP type dependent upon the associated value // given with the key in the pair. Dayle made this comment line up. if ($this->hasCast($key)) { return $this->castAttribute($key, $value); } // If the attribute is listed as a date, we will convert it to a DateTime // instance on retrieval, which makes it quite convenient to work with // date fields without having to create a mutator for each property. if ($value !== null && \in_array($key, $this->getDates(), false)) { return $this->asDateTime($value); } return $value; } /** * Append attributes to query when building a query. * * @param array|string $attributes * @return $this */ public function append($attributes) { $this->appends = array_unique( array_merge($this->appends, is_string($attributes) ? func_get_args() : $attributes) ); return $this; } /** * Set the accessors to append to model arrays. * * @param array $appends * @return $this */ public function setAppends(array $appends) { $this->appends = $appends; return $this; } /** * Return whether the accessor attribute has been appended. * * @param string $attribute * @return bool */ public function hasAppended($attribute) { return in_array($attribute, $this->appends); } /** * Get the mutated attributes for a given instance. * * @return array */ public function getMutatedAttributes() { $class = static::class; if (! isset(static::$mutatorCache[$class])) { static::cacheMutatedAttributes($class); } return static::$mutatorCache[$class]; } /** * Extract and cache all the mutated attributes of a class. * * @param string $class * @return void */ public static function cacheMutatedAttributes($class) { static::$getAttributeMutatorCache[$class] = Helper::collect($attributeMutatorMethods = static::getAttributeMarkedMutatorMethods($class)) ->mapWithKeys(function ($match) { return [lcfirst(static::$snakeAttributes ? Str::snake($match) : $match) => true]; })->all(); static::$mutatorCache[$class] = Helper::collect(static::getMutatorMethods($class)) ->merge($attributeMutatorMethods) ->map(function ($match) { return lcfirst(static::$snakeAttributes ? Str::snake($match) : $match); })->all(); } /** * Get all of the attribute mutator methods. * * @param mixed $class * @return array */ protected static function getMutatorMethods($class) { preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches); return $matches[1]; } /** * Get all of the "Attribute" return typed attribute mutator methods. * * @param mixed $class * @return array */ protected static function getAttributeMarkedMutatorMethods($class) { $instance = is_object($class) ? $class : new $class; return Helper::collect((new ReflectionClass($instance))->getMethods())->filter(function ($method) use ($instance) { $returnType = $method->getReturnType(); if ($returnType && $returnType instanceof ReflectionNamedType && $returnType->getName() === Attribute::class) { $method->setAccessible(true); if (is_callable($method->invoke($instance)->get)) { return true; } } return false; })->map->name->values()->all(); } }