wordpress: how to add hierarchy to posts - wordpress

I am creating a web-site on wordpress platform where I want to be able to post my own book texts. So what I want is to have a some kind of hierarchy where I would add a post and then add children to it (chapters). I found this:
register_post_type( 'post', array(
'labels' => array(
'name_admin_bar' => _x( 'Post', 'add new on admin bar' ),
),
'public' => true,
'_builtin' => true, /* internal use only. don't use this when registering your own post type. */
'_edit_link' => 'post.php?post=%d', /* internal use only. don't use this when registering your own post type. */
'capability_type' => 'post',
'map_meta_cap' => true,
'hierarchical' => false,
'rewrite' => false,
'query_var' => false,
'supports' => array( 'title', 'editor', 'author', 'thumbnail', 'excerpt', 'trackbacks', 'custom-fields', 'comments', 'revisions', 'post-formats' ),
) );
and tried to make the 'hierarchical"=>true, but there was no effect. Can anyone help?

Here is my workaround. This achieves exactly what you want, to be able to set post parents for the builtin post type post. You can achieve this by adding an action to the registred_post_type action hook. Just add this to your theme's functions.php.
add_action('registered_post_type', 'igy2411_make_posts_hierarchical', 10, 2 );
// Runs after each post type is registered
function igy2411_make_posts_hierarchical($post_type, $pto){
// Return, if not post type posts
if ($post_type != 'post') return;
// access $wp_post_types global variable
global $wp_post_types;
// Set post type "post" to be hierarchical
$wp_post_types['post']->hierarchical = 1;
// Add page attributes to post backend
// This adds the box to set up parent and menu order on edit posts.
add_post_type_support( 'post', 'page-attributes' );
}
There can be dozens of reasons why making posts hierarchical can be helpful. My use case is that the client wanted to structure their (already existing) posts into issues, where child posts are articles of one issue (parent posts).
This is easily achieved by limiting the query to only show posts that have no parents, using.
'post_parent' => 0,
in your query $args.

WP 4.9.*
Workaround above makes it crazy with Friendly URLs.
My solution to add hierarchy to any existent post type:
add_filter( 'register_post_type_args', 'add_hierarchy_support', 10, 2 );
function add_hierarchy_support( $args, $post_type ){
if ($post_type === 'post') { // <-- enter desired post type here
$args['hierarchical'] = true;
$args['supports'] = array_merge($args['supports'], array ('page-attributes') );
}
return $args;
}
Resave wp settings at /wp-admin/options-permalink.php

Update
Due to comments provoking new use-cases and issues, I rewrote this code and I am using it on my own sites [tested in 5.8.2]. I have provided a gist for it. You can include it in your functions.php, or make it into a plugin.
https://gist.github.com/amurrell/00d29a86fc1a773274bf049ef545b29f
🎉 This new update is leveraging SQL (fast!) to resolve the slug and post id to determine the permalink & routing. It produces the exact matching post id, even if you are using the same post_name for different post descendants. It's really fast & reliable!
In the gist, the most interesting function is get_post_from_uri($uri)
I built a custom query that will determine the permalink for us and find the exact matching post id.
Why a query? There are no wordpress functions to help determine a post's full lineage, without spinning yourself into loops.
But the relationships exist in the data!!
Therefore, the perfect way to ensure we get good friendly permalink urls is to leverage the power of the database and query language.
👇 Let's see how the query works. This may not be a perfect 1-1 of the code, because I made it dynamic, but the concept is there:
Example:
I have the following posts:
climate [547]
alliance-for-innovation [1395]
climate [1808]
procurement [518]
city-sales-cycle [1345]
climate [1811]
See it in SQL:
mysql> select id, post_name, post_parent from wp_posts where post_type = 'post' and id in (1811, 1808, 1345, 1395, 547, 518);
+------+-------------------------+-------------+
| id | post_name | post_parent |
+------+-------------------------+-------------+
| 518 | procurement | 0 |
| 547 | climate | 0 |
| 1345 | city-sales-cycle | 518 |
| 1395 | alliance-for-innovation | 0 |
| 1808 | climate | 1395 |
| 1811 | climate | 1345 |
+------+-------------------------+-------------+
Ex URL: alliance-for-innovation/climate
The full query...
mysql> select * from
-> (select TRIM(BOTH '/' FROM concat(
-> IFNULL(p3_slug,''),
-> '/',
-> IFNULL(p2_slug,''),
-> '/',
-> p1_slug
-> )
-> ) as slug,
-> id
-> from (
-> select d2.*, p3.post_name as p3_slug, p3.post_parent as p3_parent from (
-> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent
-> ) as d2
-> left join wp_posts p3 on p3.id = d2.p2_parent) as d3
->
-> ) as all_slugs
-> where slug = 'alliance-for-innovation/climate';
+---------------------------------+------+
| slug | id |
+---------------------------------+------+
| alliance-for-innovation/climate | 1808 |
+---------------------------------+------+
1 row in set (0.01 sec)
I now have both the post ID and the slug, or permalink, I should be using!
It is worth noting I went to the level of p3, which is one extra level than the URL would require (being two parts). This is to prevent something like alliance-for-innovation/climate/something from matching.
How does it work? Break down the query
There's an inside query that looks for the last part of the URL, aka basename. In this case it would be climate.
mysql> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate';
+------+---------+-----------+
| id | p1_slug | p1_parent |
+------+---------+-----------+
| 547 | climate | 0 |
| 1808 | climate | 1395 |
| 1811 | climate | 1345 |
+------+---------+-----------+
Programmatically, we keep adding abstractions around the query that's directly related to the number of / in the url, so that we can find more information about the post_parent's slug.
mysql> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent;
+------+---------+-----------+-------------------------+-----------+
| id | p1_slug | p1_parent | p2_slug | p2_parent |
+------+---------+-----------+-------------------------+-----------+
| 547 | climate | 0 | NULL | NULL |
| 1808 | climate | 1395 | alliance-for-innovation | 0 |
| 1811 | climate | 1345 | city-sales-cycle | 518 |
+------+---------+-----------+-------------------------+-----------+
After we abstracted enough times, we can then select concats as slug like: p1_slug + '/' + p2_slug
mysql> select TRIM(BOTH '/' FROM concat(
-> IFNULL(p3_slug,''),
-> '/',
-> IFNULL(p2_slug,''),
-> '/',
-> p1_slug
-> )
-> ) as slug,
-> id
-> from (
-> select d2.*, p3.post_name as p3_slug, p3.post_parent as p3_parent from (
-> select d1.*, p2.post_name as p2_slug, p2.post_parent as p2_parent from (
-> select id, post_name as p1_slug, post_parent as p1_parent from wp_posts where post_type = 'post' and post_name = 'climate'
-> ) as d1
-> left join wp_posts p2 on p2.id = d1.p1_parent
-> ) as d2
-> left join wp_posts p3 on p3.id = d2.p2_parent) as d3
->
-> ;
+--------------------------------------+------+
| slug | id |
+--------------------------------------+------+
| climate | 547 |
| alliance-for-innovation/climate | 1808 |
| procurement/city-sales-cycle/climate | 1811 |
+--------------------------------------+------+
The last step is to add a where for the original url: alliance-for-innovation/climate. And that's what you see in the full query example we first examined!
Let's see how the others go:
# climate
+---------+-----+
| slug | id |
+---------+-----+
| climate | 547 |
+---------+-----+
# procurement/city-sales-cycle/climate
+--------------------------------------+------+
| slug | id |
+--------------------------------------+------+
| procurement/city-sales-cycle/climate | 1811 |
+--------------------------------------+------+
Another thing that I like about this update is that I remembered to:
Escape the climate, or basename of the URL that we use in the query, because this is technically user-inputted (via url)
$wpdb->_real_escape($basename));
So how is this query-building function dynamic?
We use PHP arrays, loops, etc to build a string that will be the query so that we do not have to use PHP for logic about the data itself.
This is a snippet showing the dynamic abstractions - eg. how many p1_slug, p2_slug, p3_slug to grab.
// We will do 1 more depth level than we need to confirm the slug would not lazy match
// This for loop builds inside out.
for ($c = 1; $c < $depth + 2; $c++) {
$d = $c;
$p = $c + 1;
$pre = "select d${d}.*, p${p}.post_name as p${p}_slug, p${p}.post_parent as p${p}_parent from (";
$suf = ") as d${d} left join $wpdb->posts p${p} on p${p}.id = d${d}.p${c}_parent";
$sql = $pre . $sql . $suf;
$concats[] = sprintf("IFNULL(p${p}_slug,'')");
}
Previous Answer:
I came here looking to achieve:
Adding page attributes to post_type posts to add parent posts
Being able to add a page template to post_type posts
Being able to get hierarchical permalink structure on post_type posts
I was able to use the accepted answer to accomplish 1 & 2, but not 3.
Note: to fully get 2 to work, you need to specify the post_type in the template comments of your page template like this:
<?php
/*
Template Name: Your Post Template Name
Template Post Type: post
*/
For 3, I found a plugin that ruined my post_type pages, and it was a lot of pretty awful, unmaintained code.
So I wrote a solution to accomplish all this, borrowing from this answer:
(Tested with 4.9.8)
<?php
add_action('registered_post_type', 'make_posts_hierarchical', 10, 2 );
// Runs after each post type is registered
function make_posts_hierarchical($post_type, $pto){
// Return, if not post type posts
if ($post_type != 'post') return;
// access $wp_post_types global variable
global $wp_post_types;
// Set post type "post" to be hierarchical
$wp_post_types['post']->hierarchical = 1;
// Add page attributes to post backend
// This adds the box to set up parent and menu order on edit posts.
add_post_type_support( 'post', 'page-attributes' );
}
/**
* Get parent post slug
*
* Helpful function to get the post name of a posts parent
*/
function get_parent_post_slug($post) {
if (!is_object($post) || !$post->post_parent) {
return false;
}
return get_post($post->post_parent)->post_name;
}
/**
*
* Edit View of Permalink
*
* This affects editing permalinks, and $permalink is an array [template, replacement]
* where replacement is the post_name and template has %postname% in it.
*
**/
add_filter('get_sample_permalink', function($permalink, $post_id, $title, $name, $post) {
if ($post->post_type != 'post' || !$post->post_parent) {
return $permalink;
}
// Deconstruct the permalink parts
$template_permalink = current($permalink);
$replacement_permalink = next($permalink);
// Find string
$postname_string = '/%postname%/';
// Get parent post
$parent_slug = get_parent_post_slug($post);
$altered_template_with_parent_slug = '/' . $parent_slug . $postname_string;
$new_template = str_replace($postname_string, $altered_template_with_parent_slug, $template_permalink);
$new_permalink = [$new_template, $replacement_permalink];
return $new_permalink;
}, 99, 5);
/**
* Alter the link to the post
*
* This affects get_permalink, the_permalink etc.
* This will be the target of the edit permalink link too.
*
* Note: only fires on "post" post types.
*/
add_filter('post_link', function($post_link, $post, $leavename){
if ($post->post_type != 'post' || !$post->post_parent) {
return $post_link;
}
$parent_slug = get_parent_post_slug($post);
$new_post_link = str_replace($post->post_name, $parent_slug . '/' . $post->post_name, $post_link);
return $new_post_link;
}, 99, 3);
/**
* Before getting posts
*
* Has to do with routing... adjusts the main query settings
*
*/
add_action('pre_get_posts', function($query){
global $wpdb, $wp_query;
$original_query = $query;
$uri = $_SERVER['REQUEST_URI'];
// Do not do this post check all the time
if ( $query->is_main_query() && !is_admin()) {
// get the post_name
$basename = basename($uri);
// find out if we have a post that matches this post_name
$test_query = sprintf("select * from $wpdb->posts where post_type = '%s' and post_name = '%s';", 'post', $basename);
$result = $wpdb->get_results($test_query);
// if no match, return default query, or if there's no parent post, this is not necessary
if (!($post = current($result)) || !$post->post_parent) {
return $original_query;
}
// get the parent slug
$parent_slug = get_parent_post_slug($post);
// concat the parent slug with the post_name to get most of the url
$hierarchal_slug = $parent_slug . '/' . $post->post_name;
// if the concat of parent-slug/post-name is not in the uri, this is not the right post.
if (!stristr($uri, $hierarchal_slug)) {
return $original_query;
}
// pretty high confidence that we need to override the query.
$query->query_vars['post_type'] = ['post'];
$query->is_home = false;
$query->is_page = true;
$query->is_single = true;
$query->queried_object_id = $post->ID;
$query->set('page_id', $post->ID);
return $query;
}
}, 1);
You can save this to a file custom-posts-hierarchy.php and include it in your functions.php file in your theme, or you can add to the top:
/*
Plugin Name: Custom Posts Hierarchy
Plugin URI:
Description: Add page attributes to posts and support hiearchichal
Author: Angela Murrell
Version:
Author URI:
*/
And drop it into your plugins folder. Good luck!

Posts in Wordpress are supposed to be typical chronological Blog Posts. Pages are made for static content, they can be organised in a hierarchical structure out of the box.
For any Page, you can select a parent page. This way, you can create nested hierarchies with multiple children. Sounds like what you need.
Check the Wordpress Documentation for details.
If you have a deep, complicated tree structure, a plugin might help you manage it, like Wordpress Page Tree. It provides a better interface than the default Wordpress Page listing.

Using a plugins like CPT UI, you can create a custom post type and set it to have hierarchical tree.
Then just check that the post type page-attribute is set for this custom post type and voile, your posts now have hierarchical states.
https://wordpress.org/plugins/custom-post-type-ui/

best solution is to create custom taxonomy [1]: http://codex.wordpress.org/Function_Reference/register_taxonomy and create main slug - books or something else.

There exists plugin, which creates hierarchy for post:
https://wordpress.org/plugins/add-hierarchy-parent-to-post/

Isn't this a better option?
register_post_type( 'MYPOSTTYPE',
array(
'labels' => array(
'name' => __( 'MYPOSTTYPE' ),
'singular_name' => __( 'MYPOSTTYPE' )
),
'supports' => array('title', 'editor', 'page-attributes'),
'public' => true,
'has_archive' => true,
'hierarchical' => true,
'rewrite' => array('slug' => 'MYPOSTTYPE'),
)
);
I have added:
'hierarchical' => true,
and it works.

I was also considering to add hierarchy to posts and I read through this topic. My conclusion is that (in my opinion) it is better to avoid adding hierarchy to posts. Post and page hierarchy is such a core concept of Wordpress, and all kinds of issues with SEO and compatibility with plugins (and Wordpress core) can come up when touching this.
Instead of implementing a complicated technical solution, and having to hope that it remains compatible with future Wordpress and plugin updates, it seems better to use categories and subcategories for hierarchy of posts. And if that doesn't provide sufficient flexibility, use pages instead of posts because pages also come with support for hierarchy out of the box (as previously mentioned by others).
There may be situations where it looks like post categories or pages both can't solve your hierarchy problem but I'd still argue that you should consider whether you want to complicate things for yourself by implementing some form of customized post hierarchy solution, and all the future problems that can come from it.
While it is tempting to come up with a technical solution for a problem, sometimes the best answer is to not do something at all and use a different approach altogether. I strongly believe that is the case here.

Related

Add WP Permastruct which contain a category slug and a year

I try to create a custom rewrite rule in a Wordpress site to obtain a list of posts from a specific category posted in a specific year and I just don't know how. Here's how I added the rules in init action.
$wp_rewrite->add_rewrite_tag("%muz_category_name%", '([^/]+)', "category_name=");
$wp_rewrite->add_rewrite_tag("%muz_year%", '([0-9]{4})', "year=");
$wp_rewrite->add_permastruct( 'category-archive', 'category-archive/%muz_year%/%muz_category_name%', array( 'walk_dirs' => false, 'paged' => true ) );
For example, there are 3 posts (with publish state) posted in 2021 year and having category Expozitii temporare. The category slug is expozitii-temporare. So, when I access the URL /category-archive/2021/expozitii-temporare I get a 404 page and checking SQL queries I get the following query:
SELECT ID, post_name, post_parent, post_type
FROM muz_posts
WHERE post_name IN ('2021','expozitii-temporare')
AND post_type IN ('page','attachment')
If I switch parameters order in the permastruct as follows:
$wp_rewrite->add_permastruct( 'category-archive', 'category-archive/%muz_category_name%/%muz_year%', array( 'walk_dirs' => false, 'paged' => true) );
in the SQL queries I get:
SELECT ID, post_name, post_parent, post_type
FROM muz_posts
WHERE post_name IN ('2021')
AND post_type IN ('post','attachment')
and
SELECT muz_posts.* FROM muz_posts WHERE 1=1 AND muz_posts.post_name = '2021' AND muz_posts.post_type = 'post' ORDER BY muz_posts.post_date DESC
So, nothing by checking post_date column or left joining posts table with term_relashionships in search for category slug or something.
After a couple of hours, any start point is much appreciated.
Thanks in advance!
PS: Well, I have a list of years for each category, in this example Expozitii temporare. Some of the articles have been imported from another blog but I kept the initial publish date. It seems that only the years containing articles published with older date have issues and return 404.

Wordpress search optimization: limit search result (not post_per_page)

I have searched stackoverflow. none of them apply to my question. Most of the answer just about limit per page (how many posts to show per page, not how many result to search)
I'm trying to speed up wordpress search.
For example I have 10millions post to search. and user type "a". It will take very long to search all of that because wordpress is using SQL_CALC_FOUND_ROWS.
The fact is that there is no need to search milions of rows (posts) and the answer "1000+" is enough for users. So we need to stop search after we found 1000 results.
The only way i can think of is to use:
'no_found_rows' => true
But then we dont have pagination functionality.
Or maybe we can use a separate query to get the result and to count how many posts found (with limiter)?
I dont understand how can wordpress doesnt have this functionality. Even big site like amazon limit the search result. So if you search "a" or "usb", it will return just the first 20 pages.
Try this code, its works on my wordpress with 1M posts
If you use search for custom post type, change
post_type = 'post'
to your post type name
add_filter('pre_get_posts', 'fix_search_pagination_with_nfr', 1000);
function fix_search_pagination_with_nfr() {
global $wp_query, $wpdb;
if ( ! is_admin() && $wp_query->is_main_query() && $wp_query->is_search() ) {
$wp_query->query_vars['no_found_rows'] = 1;
$wp_query->found_posts = $wpdb->get_var( "SELECT COUNT(*) FROM wp_posts WHERE 1=1 AND wp_posts.post_type = 'post' AND (wp_posts.post_status = 'publish' OR wp_posts.post_status = 'private')" );
$wp_query->found_posts = apply_filters_ref_array( 'found_posts', array( $wp_query->found_posts, &$wp_query ) );
if($wp_query->query_vars['posts_per_page'] > 0) {
$wp_query->max_num_pages = ceil($wp_query->found_posts / $wp_query->query_vars['posts_per_page']);
} else {
$wp_query->max_num_pages = 0;
}
}
return $wp_query;
}

WP All Import Pro: Polylang compatibility

I'm trying to import data from an Excel sheet into WordPress with the Pro version of WP All Import. We are using Polylang for multi language support. I wonder how to manage importing the content into the correct language versions. I discovered that there is a hidden taxonomy "language" which I can manually set to "de" for setting language to German. But how do I link corresponding translations? Any ideas how to solve this, maybe with some custom functions? Thank you in advance!
I have found a better solution.
if ( !function_exists( 'pll_save_post_translations' ) ) {
require_once '/include/api.php'; }
$arr = array();
$result = pll_save_post_translations(['en' => 21398, 'ro'=>21396]);
//where 21398 and 21396 are the post_id and 'en' and 'ro' the nicenames of the languages.
Put this code in your functions.php and run it only once (meaning you refresh the page only once as admin after you put it). Then delete the code from functions.php. It does not actually create errors, just don't risk it, don't run it twice.
Before you do that, you must import the products in both (in my case) languages. Meaning you import them in one language (you select one language from the top admin bar) and then in another session you import them in the other language (you select the other language) and also if you see "Show private taxonomies" in WP All import, put one language code there. One for each session. After the products in both languages have been imported, you run the code above to tell WordPress that "this post_id is the translation of that post_id".
I hope it helps.
Ah, and you get the post_id in a nice table which you can edit in excel by exporting the products with Wordpress Export (it is pre-installed when you install WordPress, I think). You find it in Dashboard in Tools/Export.
And put
$result = pll_save_post_translations(['en' => 56465, 'ro'=>654864]);
as many times as you need
$result = pll_save_post_translations(['en' => 9999, 'ro'=>34654]);
$result = pll_save_post_translations(['en' => 98641, 'ro'=>98651]); .
for each correlation.
You can do it easier in Excel, you have the correlating id's on two separate columns then put this formula on the next row
=concatenate("$result = pll_save_post_translations(['en' =>",CELL A1,"
'ro'=>",CELL B1,"]);")
Apply it downwards. Select the column you have just concatenated. Paste it under the code in functions.php. Save. Refresh. Delete code from functions.php. Refresh. Booyah.
Please note that I'm currently using both Polylang and Wp All Import/Export Pro versions.
Also, this is currently not 'outdated' code. It relies on an undocumented feature, that was suggested directly from the wp all import team. I'm speaking simply of the additional parameter $data passed by the pmxi_saved_post action.
That said, I know polylang devs are currently working on an addon for this specific issue. Anyway, I managed to do it in this way, for now:
1 -> when you export data, there are 2 polylang-related fields: languages and post_translations. Export them too.
2 -> when you import your data, on the wpallimport screen, create 2 more custom fields and save the 2 above. See attachment below. Now you've got the translations data with every post.
3 -> set this filter:
global $language_codes;
$language_codes = array( // sample lang data
'italiano' => 'it'
,'english' => 'en'
,'espanol' => 'es'
,'francais' => 'fr'
,'deutsch' => 'de'
);
add_action('pmxi_saved_post', 'set_imports_lang', 10, 2);
// using undocumented param $data
// action firm is the following (in fact, it passes 3 params): do_action( 'pmxi_saved_post', $pid, $rootNodes[$i], $is_update );
function set_imports_lang($post_id, $data){
global $language_codes;
// 'lingue' is italian, I guess it will be 'languages' in english: it's one of the 2 fields we saved before
pll_set_post_language($post_id, $language_codes[sanitize_title($data->lingue)]);
}
At this point you have just set each post's original language, nothing more. Now we have to link translations each other. We'll do this with a function that we have to run ONLY 1 TIME. It will run simply reloading any wp screen/page.
function set_imports_translations(){
global $wpdb;
global $language_codes;
// substitute 'enews' with your own post type
$list = $wpdb->get_results("
SELECT
m.post_id, m.meta_value pll, group_concat(concat_ws(':', m2.meta_value, m.post_id)) ids
FROM
$wpdb->posts p
INNER JOIN $wpdb->postmeta m ON p.ID = m.post_id
INNER JOIN $wpdb->postmeta m2 ON m.post_id = m2.post_id AND m2.meta_key = '_import_language_name'
WHERE
p.post_type = 'enews' AND m.meta_key = '_import_translations_id'
GROUP BY pll
");
// query results are something like this:
// 10258 pll_57e92254c445f 10258:Italiano,10259:English,10260:Español,10261:Français,10262:Deutsch
// 10263 pll_57e922552b8c6 10263:Italiano,10264:English,10265:Español,10266:Deutsch
// 10267 pll_57e9225587124 10267:Italiano
// extract data from the third column string
foreach($list as $item){
$ids = explode(',',$item->ids);
$list = array();
foreach($ids as $id){
$data = explode(':',$id);
$list[$language_codes[sanitize_title($data[0])]] = intval($data[1]);
}
//set the translations
pll_save_post_translations($list);
}
}
set_imports_translations();
That's all. :) Ah, the attachment mentioned above:

Wordpress - showing last uploaded images, but only from a specific custom post type

I have a Wordpress-powered website with several custom post types, and I'm struggling to find a way to load the latest uploaded images from one of the custom post types only.
In other words, I know how to ask for the most recent 4 images added to the Wordpress media library, but I can't find a way to filter the attachments depending of the post type of their parent page (in this case, show only the images uploaded to 'Image Gallery' posts - not regular posts, not 'Video Gallery' posts, etc).
It feels like a very basic question, but for some reason I can't get this work and I'm really frustrated. Is there any way to get the post type of the post parent in the query? Or I should approach this issue from another angle?
I don't believe it's possible to use WP_Query to filter based on the post type of the parent. The most straightforward way around this is to use SQL and $wpdb->get_results() directly. This will also bypass all built in caching, etc so think about how your implementation will be used.
global $wpdb;
// the SQL statement to only fetch the N last attachments
$sql = <<<SQL
SELECT attachments.*
FROM {$wpdb->posts} attachments
-- join the parent post based on the parent_post value to filter by cpt
JOIN {$wpdb->posts} post
ON post.ID = attachments.post_parent
AND post.post_status = 'publish'
-- pass in the custom post type
AND post.post_type = %s
WHERE
-- only fetch images, double percents for prepare()
attachments.post_mime_type LIKE 'image%%'
ORDER BY attachments.post_date DESC
-- pass in the number to fetch
LIMIT %d
SQL;
$cpt = "your_custom_post_type";
$limit = 4;
$attachments = $wpdb->get_results( $wpdb->prepare( $sql, $cpt, $limit ) );
foreach ( $attachments as $attachment ){
// $attachment will have all the rows from the posts table.
$id = $attachment->ID;
}

Wordpress get attachment of post doesn't work

I'm trying to retrieve all the attachment of a specific post, but it doesn't work for the moment. Here is the code :
$args = array(
'post_type' => 'attachment',
'posts_per_page' => -1,
'post_parent' => $id
);
$attachments = get_posts($args);
Where Id is the id of my post. I've tried also with new WP_Query() but it didn't worked neither. Both ways return an empty result.
Does anyone have an idea what I'm doing wrong here ?
Thanks
EDIT
Ok, I think I know what's going wront.
When using this arguments for the get_posts function, it will return the images uploaded through the "Add a media" button in the specific post.
So basically, let's say that on my first post, I've uploaded all the images I would need for all my future post. If I apply the request on the first post, I will retrieve all the images, even the one that I don't use in this post.
If I apply this function to another post, because I didn't uploaded any file in this post, the function will retrieve an empty array.
Does anyone have an idea how I can retrieve all the images used in a specific post ? So not only uploaded, but really integrated into the post or added to a custom field ?
Thanks
When using get_posts to fetch attachments, you need to set post_status to inherit. Also, check out the get_children function:
$args = array(
'post_type' => 'attachment',
'post_mime_type' => 'image',
'numberposts' => -1,
'post_status' => 'inherit',
'post_parent' => $id
);
$attachments = get_children( $args );
Case: using ACF, I created an repeater field 'resort_pics' for my gallery, which has 2 fields 'picture_title' as text and 'picture' as an picture type.
Then happily uploaded 100 photos, some of them were same for several posts so I only clicked those from uploaded gallery (I will use term of "referenced images" for those).
Problem : then one happy day I noticed all "referenced images" are missing in our api.
Cause :
As noted at documentation comment from 'Uriahs Victor' (https://developer.wordpress.org/reference/functions/get_attached_media/)
Important to note that this function only returns the attachments that
were first uploaded/added to the post. Uploading an image to Post A(ID
1) and then adding that image later to Post B(ID 2) would give an
empty array if the following code was used:
$media = get_attached_media( 'image', 2 );
var_dump( $media );
Real cause :
Real source of all this problem is The thing that information about "reference images" are not stored in 'wp_posts' table but are actually stored in 'wp_postmeta', so by just querying 'wp_posts' table or using get_attached_media() which only looks there also you will not get all attachements for post.
Solution :
Lets take an example of post with ID - $postId which has defined
repeater - 'resort_pics', field in repeater with image 'picture'. (created with ACF plugin)
First get all attachements for our post (resort) (including images/pdfs and so on) with classic way (can be also used 'get_attached_media'):
$images = $wpdb->get_results("select ID,guid from wp_posts where post_parent = " . $postId . " and `post_type`= 'attachment'");
where guid is 'url' to an attachement, lets index those in array where key will be post id of an attachement
$mapImages = [];
foreach ($images as $image) {
$mapImages[$image->ID] = $image->guid;
}
Now we have all atachements but are missing all referenced images/files.
Lets continue by selecting all meta for our post (resort).
$metas = $wpdb->get_results("select meta_key,meta_value from wp_postmeta where post_id=" . $postId);
And index them by an meta key
$mapMetas = [];
foreach ($metas as $meta) {
$mapMetas[$meta->meta_key] = $meta->meta_value;
}
Lets say our post ($postId) has an 9 entries in 'resort_pics' with an image uploaded to its 'picture' field, then $mapMetas['resort_pics'] will have an value of 8.
Now the way how repeater fields keys are stored in $mapMetas array, is actually an :
'resort_pics_0_picture' -> itsPostId (5640 for example)
'resort_pics_1_picture' -> itsPostId (5641 for example)
'resort_pics_2_picture' -> itsPostId (5642 for example)
...
'resort_pics_8_picture' -> itsPostId (5648 for example)
Knowing this we can get simply all image urls for "resort_pics"
for ($i = 0; $i < $mapMetas['resort_pics']; $i++) {
$picture = [];
$picture['name'] = $mapMetas['resort_pics_' . $i . '_picture_title'];
$picture['url'] = $mapImages[$mapMetas['resort_pics_' . $i . '_picture']];
$pictures[] = $picture;
}
You may already get to this point, simply from $mapMetas get image ID and using it get an image url from $mapImages.
Lets say 'resort_pics_1_picture' is 'referenced' one (not directly uploaded image), we have its id '5641' but since its not connected to our $postID but to some other post id when it was actually uploaded. Its missing in our $mapImages array, so lets edit this code a bit.
for ($i = 0; $i < $mapMetas['resort_pics']; $i++) {
$picture = [];
$picture['name'] = $mapMetas['resort_pics_' . $i . '_picture_title'];
$picture['url'] = getAttachmentOrItsReferenceUrl('resort_pics_' . $i . '_picture', $mapImages, $mapMetas);
$pictures[] = $picture;
}
We have added an getAttachementOrItsReferenceUrl() method, which will simply first check if we already have this image (all uploaded to this post) and if not will fetch image data by its post id from 'wp_posts' table..
function getAttachmentOrItsReferenceUrl($code, $allAttachmentById, $allPostMetaByKey) {
global $wpdb;
$attachmentUrl = $allAttachmentById[$allPostMetaByKey[$code]];
if($attachmentUrl === null && isset($allPostMetaByKey[$code]) && $allPostMetaByKey[$code] !== '') {
$attachments = $wpdb->get_results("select ID,guid from wp_posts where ID = " . $allPostMetaByKey[$code]);
foreach ($attachments as $image) {
return $image->guid;
break;
}
} else {
return $attachmentUrl;
}
return "";
}
Finnal thoughts :
If you know your fields structure you can build up its key pretty straightforward
For an example
'rooms' repeater which has inside 'room_gallery' repeater which has inside 'image' image field.
'rooms_0_room_gallery_0_image'
--
$mapMetas['rooms'] - number of rooms
$mapMetas['rooms_' . $i . '_room_gallery'] - number of gallery images for room
--
'rooms_' . $i . '_room_gallery_' . $j . '_image'
How heavy is it ? Not really, if you are like me, having only small amount of referenced images you wont even feel it. All we added to load is an one meta query for an post, if no images are referenced that's all, then for every referenced image there is one more query, but its query on ID should be fast. There is a lot of way how to make load less, for example not do query for every single missing image, but do single query at the end with 'where in (id1,id2..)' to have single query to get all, or after fetching new image data, store it in some global array by its id, and check this array before fetching image for next posts (so if one image is stored in 200 posts, and you are fetching all it would make only 1 query), you got the point I leave it just at the ideas since this is bloody long post even without that :)
Hope this will help anybody in future.

Resources