• Resolved cxtinac

    (@cxtinac)


    I have a custom post type, with several custom meta values. ‘Add’ and ‘update’ for this CPT are working fine from the Admin UI.

    Now I need to import multiple records from an external source, and I have an external script which POSTs to accomplish this.

    I have tested many times POSTing a single record from my external script and it appears to be working as expected. i.e. record is created if it does not exist, meta values are added and values are set correctly. Repeating this one single POST shows update_post_meta() returning false in my debugging as expected (since values already exist), and the meta values continue to appear correctly in the MySQL database and Admin UI. However..

    Here is my problem: When I invoke my external script with two or more CPT updates in two POSTs, previously correct values for all all except the last update appear empty in the Admin UI, and NULL in the database.

    My external script sends stringified json in the POST so I have this:

    if ($_SERVER['REQUEST_METHOD'] == "POST"){
    	$json = file_get_contents('php://input');
    	if ($json){
    		$data = json_decode($json, true);
    		$registry = new My_Registry($data);
    		$registry->create_or_update();
    	}
    }

    -and here is the create_or_update() method that is called:

    function create_or_update(){
    	$post_id = $this->_exists();
    	if ( ! $post_id  ){
    		$post_id = wp_insert_post(array (
    			'post_type' => 'gwc_registry',
    			'post_title' => $this->title,
    			'post_status' => 'publish',
    			'comment_status' => 'closed',
    			'ping_status' => 'closed'
    		));
    	}
    	if ( $post_id ){
    		$this->registry_cpt_id = $post_id;
    	}
    	$this->_update_meta();
    }

    _exists() returns false or the post_id if found (I’m trying to keep code lines down, but can share if helpful).

    Here is the essence of _update_meta()

    private function _update_meta(){
    	$cpt_meta_fields = (array)$this;
    	foreach ($cpt_meta_fields as $key => $value){
    		$clean_key = (explode("\0", $key))[2];
    		$registry_meta_key = 'registry_' . $clean_key;
    		var_dump($registry_meta_key . " > " . $value); //**see comment below
    //....//
    		update_post_meta( $this->registry_cpt_id, $registry_meta_key, $value );
    	}
    }

    The debug line at ** always shows correct meta data right before the call to update_post_meta() in my testing, but when I post two records from my external script the first one still gets overwritten with NULLS as described at the top.

    This is not a timing issue – I have tried putting a delay in my external script, and I can watch the correct meta data get written into the database for the first record and subsequently get set to NULL when the second record gets processed – even though update_post_meta() returns false for that ‘nullified’ record at the same time!

    This appears to be something in the way I am using update_post_meta here but I am at a loss as to how to proceed, and would really appreciate any hints as to where I might dig next.

    Thank you in advance for any advice..

Viewing 14 replies - 1 through 14 (of 14 total)
  • The code looks OK, but I can suggest a few things.

    1. Do empty check in $post_id

    if ( empty( $post_id )  )
    // Then insert post

    2. Update meta when you have post id.

    if ( ! empty( $post_id )  )
        $this->registry_cpt_id = $post_id;
        $this->_update_meta();
    }

    3. I am not able to understand this line
    $cpt_meta_fields = (array)$this;
    $this is a class object and you’re converting it into an array, doesn’t make sense to me.

    4. About the NULL issue, I am not sure about it. You should perform all empty checks in the required places to make sure everything is running after condition.

    Thread Starter cxtinac

    (@cxtinac)

    Thank you very much!
    It’s good advice, I will try NULL trapping the updates as you suggest, and check for empty $post_id.

    3. I am not able to understand this line
    $cpt_meta_fields = (array)$this;
    $this is a class object and you’re converting it into an array, doesn’t make sense to me.

    The class in fact contains more than 20 attributes, and I am using this to convert to a key value array, without having to explicitly access each, which I can then pass to the meta update.
    It’s analogous to
    get_object_vars($obj)
    but includes the (many) private attributes.

    Thanks again

    Thread Starter cxtinac

    (@cxtinac)

    Unfortunately this did not resolve the problem. I have cleaned up the code, added empty() checks as suggested and wrapped the call to update_post_meta() in a further check:

    function _write_meta_value( $post_id, $meta_key, $meta_value){
    	if ( ! isset($post_id, $meta_key, $meta_value) )
    		return null; //the only value update_post_meta will NOT return
    	return update_post_meta( $post_id, $meta_key, $meta_value );
    }

    Unfortunately this works exactly the same as the original problem statement.

    I also briefly enabled MariaDB query logging on the development server and for the first of two POSTs from my external script (see above) I get this (MariaDB fieldname backticks removed for clarity):

    2410 Query	INSERT INTO wp_posts (post_author, post_date, post_date_gmt, post_content, post_content_filtered, post_title,......
    2410 Query	INSERT INTO wp_postmeta (post_id, meta_key, meta_value) VALUES (2842, 'registry_id', '378')

    Note post ID 2842.

    For the second of two POSTs I see this:
    2416 Query INSERT INTO wp_posts (post_author, post_date, post_date_gmt, post_content, post_content_filtered, post_title,.......
    Post ID is 2843.
    But then I get:
    2416 Query UPDATE wp_postmeta SET meta_value = NULL WHERE post_id = 2842 AND meta_key = 'registry_id'
    Note this is setting meta to NULL for the previous post 2842!

    I feel I must be misunderstanding something fundamental about update_post_meta (although I have used it many times in the past without encountering anything close to this).

    Is it possible this is a concurrency issue?

    Any and all follow up very welcome.

    Edit: markup not allowed inside code marks

    • This reply was modified 2 years, 10 months ago by cxtinac.

    Dump the $cpt_meta_fields variable and review all the data it has. The problem seems from data not from the functions.

    Moderator bcworkz

    (@bcworkz)

    If $this->_exists() fails to return an ID, $this->registry_cpt_id will be the value of the previously inserted post. The server may not even be receiving the data as expected. How are you POSTing this data?

    Thread Starter cxtinac

    (@cxtinac)

    @bcworkz thank you, that is a very interesting thought.

    Briefly the data is in fact POSTed from a nodejs system that receives, parses and scrubs data from external systems before POSTing it to this code.
    I have validated the contents of the json before it gets sent many times as part of this whole debugging process (I do not have an example to hand, but could generate one if it’s helpful).

    Here’s the latest version of the POST handler code (changed slightly from in the original post above, as part of my ongoing efforts):

    // we might be getting POSTed json data from the back end sync
    if ($_SERVER['REQUEST_METHOD'] == "POST"){
    	$json = file_get_contents('php://input');
    	if ($json){
    		$data = json_decode($json, true);
    		$registry = new My_Registry($data);
    		$registry->create_or_update();
    		unset($registry);
    		exit();
    	}
    }

    I think the unset() and exit() are not needed, but not causing further issues.

    I’ve added a debug var_dump() to the _write_meta_value() function above (3 posts up), and that has only ever shown valid data. But having said that, what you describe is exactly consistent with the problem I am having. Here’s the full code that decides whether to create or update. I believe this should always end up with a valid value for $this->registry_cpt_id but I have stared at it so much I may well have missed something:

    function create_or_update(){
    	$post_id = $this->_exists();
    	if ( empty($post_id) ){
    		$post_id = wp_insert_post(array (
    			'post_type' => 'gwc_registry',
    			'post_title' => $this->title,
    			'post_status' => 'publish',
    			'comment_status' => 'closed',
    			'ping_status' => 'closed'
    		));
    	} else {
    		// TODO update existing post title here if necessary
    	}
    	// do we now have a post to work with?
    	if ( ! empty($post_id) ){
    		$this->registry_cpt_id = $post_id;
    		$this->_update_meta();
    	}
    }
    
    private function _exists(){
    	$params = array(
    		'posts_per_page' => 1,//only looking for one hit
    		'post_type' => 'gwc_registry',
    		'meta_key' => 'registry_id',
    		'meta_value' => $this->id
    	);
    	$reg_query = new WP_Query($params);
    	if ($reg_query->have_posts()) {
    		$reg_query->the_post();
    		$post_id = get_the_id();
    		wp_reset_postdata();
    		return $post_id;
    	}
    	return false;
    }

    Unfortunately importing this data from multiple sources (which I have no control over) means there are multiple unique index key fields that need to be preserved. So $this->registry_cpt_id is the WordPress post ID, different from $this->id and $this->registryno.

    Sorry for the long post, and thanks again

    Will be nice if you can share the complete PHP class file code with gist or Github. so that we could review your complete code.

    Thread Starter cxtinac

    (@cxtinac)

    Thanks @vijayhardaha that’s a good thought.
    Let me look in to that (obviously there’s some private content I have to filter out).

    Moderator bcworkz

    (@bcworkz)

    While you’re posting your code to Gist. please include a couple sample JSON strings being used as source data. Obfuscate any sensitive data of course.

    Thread Starter cxtinac

    (@cxtinac)

    I’ve put what I believe is everything relevant in a Gist.
    A few comments:

    • I’ve included the small awk script I used to remove proprietary / personal info from the example POST json strings. This was run with a final sed cleanup like this – ./awk-cleanup.sh post1.txt | sed 's/,}/}/g' > post1-clean.txt
    • I believe the result is still valid json, but I have not tested that (it’s a fine tradeoff between a good result and writing a custom json parser obviously)
    • I’ve also included the my_dump.php utility that is referenced many times (commented out) in the code
    • I have pasted the cleaned up code without removing my code comments, and also left a few commented lines of code in the hope it might be helpful as well.
    • Sorry for the lengthy turnaround – it took some time to put this all together in a shareable form.

      Edit: One final comment in case it’s relevant, the POST is going to a custom archive page for the Registry CPT;

    • This reply was modified 2 years, 10 months ago by cxtinac.

    I have cleaned your registry class code a little bit here Gist url

    I am still not able to understand $cpt_meta_fields = (array)$this; this. maybe you’re creating so many class variables then filtering them somehow, it’s better to set all the data into an array variable with mapping then loop through the array.

    I’ve changed the post check function to reduce to DB query load, also for number sanitize using absint and for text field using sanitize_text_field.

    • This reply was modified 2 years, 10 months ago by Vijay Hardaha. Reason: Fixing url value
    Thread Starter cxtinac

    (@cxtinac)

    Thank you very much @vijayhardaha I have looked at your Gist.

    These are good cleanup suggestions, I will do this. I guess my question would be do you believe this will help with the original problem? Obviously there are many ways to wrangle the incoming stringified json POST into update_post_meta() calls, but I still have been unable to find any evidence that I’m sending invalid data to update_post_meta() (I wish I had!).

    I am still not able to understand $cpt_meta_fields = (array)$this; this.

    The My_Registry class is a pretty thin wrapper on the $data array anyway, and (array)$this then is a one-line way to get the data array back out, without getting backed in to iterating over dozens of fields by name. The (admittedly small) advantage of actually parsing $data into private fields in the class in the first place is that it will help me cleanly reference individual fields as needed in future code. The object “unpacking” happens in a private method, and obviously values are referenced read-only fwiw.

    There is a detailed discussion of this approach here, in case you have not seen that.
    Hope that helps.

    Thanks again for your advice.

    Thread Starter cxtinac

    (@cxtinac)

    I have rewritten this as a straightforward linear POST handler, and I am getting the exact same behavior. It is added to the Gist here.

    This code blindly adds a new CPT post every time, without checking whether it is a duplicate post or not.

    If I POST identical json twice I get two posts created, both with the meta data correctly populated, as expected. However, if I POST different json the second time the second post is correctly populated, but the first post meta data then gets wiped in the same single POST handling.

    I still worry that I’m missing something fundamental in the way update_post_meta works. It seems like post ID and meta_key are not sufficient selection criteria for the database transaction. Put another way, update_post_meta( int $post_id, string $meta_key, mixed $meta_value ) is using something from its context to decide what to do.

    Edit: clarification

    • This reply was modified 2 years, 10 months ago by cxtinac.
    • This reply was modified 2 years, 10 months ago by cxtinac.
    Thread Starter cxtinac

    (@cxtinac)

    Fixed.
    The problem was that the custom post type was using a global $post; to get a post ID to operate on when it ran update_post_meta() (old code), and when a new custom post was created it leaked a reference to the prior post, leading to the confusing results I experienced.

    So for me the learning is be exceptionally cautious about scope when using global $post;, especially outside of theme files and the loop!

    Here’s the discussion that put me on to the solution.

    @vijayhardaha @bcworkz thank you very much for your help, advice and support!
    I will update the Status to resolved.

Viewing 14 replies - 1 through 14 (of 14 total)
  • The topic ‘update_post_meta() data appears to be overwritten with NULL’ is closed to new replies.