1

i am trying to add options with category specific names to all categories individually. I spent many hours on this and can't find a solution. This is my code:

add_action( 'category_edit_form_fields', 'my_category_custom_fields_x' );
add_action( 'edit_category', 'save_my_category_custom_fields_x' );

function my_category_custom_fields_x( $cat ) {
    
    $category_meta = get_option( 'category_meta_'.$cat->term_id );
    
    ?>
    <tr class="form-field">
        <th scope="row" valign="top"><label for="category-title"><?php _e("Title"); ?></label></th>
        <td>
        <input id="category-title" name="category_meta_[<?php echo $cat->term_id ?>]"
        value="<?php if ( isset( $category_meta[ $cat->term_id ] ) ) esc_attr_e( $category_meta[ $cat->term_id ] ); ?>" />
        </td>
    </tr>
<?php
}

function save_my_category_custom_fields_x() {
    if ( isset( $_POST['category_meta_'] ) &&
    !update_option('category_meta_'.$_POST['category_meta_'], $_POST['category_meta_']) )
    add_option('category_meta_'.$_POST['category_meta_'], $_POST['category_meta_']);
}

This is the result in the database: For option name i get:

category_meta_Array

and for option value:

a:1:{i:7;s:5:"testX";}

How do i get the category id to show in the option name? The $_POST object is an array, right? How can i get hold of $cat->term_id? Isn't it part of the array?

category_meta_7

With the above function the input is always empty. How do i show 'textX' in the input field when the option is saved on edit_category?

2 Answers 2

1

There are 4 problems a small one, a security problem, a misunderstanding, and a big problem.

The Small Problem

Because you used [] notation in the name you've indicated that there are multiple category_meta_ values:

category_meta_[<?php echo $cat->term_id ?>]

Which means $_POST['category_meta_'] will always be an array.

It would be much easier to append the term ID, or remove it entirely than it would be to wrap it in [] characters.

The Security Problem

This is not good:

esc_attr_e( $category_meta[ $cat->term_id ] )

I can give you points for escaping, but this code is the same as:

echo esc_attr( __( $category_meta[ $cat->term_id ] ) );

Which commits a major mistake of passing dynamic data into a static string localisation API. Never pass dynamic strings into __( type functions, be it directly or indirectly. echo esc_attr should have been used.

The Misunderstanding

This:

if ( ... && !update_option(...) )
    add_option(...);

Works the same as this:

if ( ! ... ) {
    return;
}
update_option(...)

update_ type functions will add the option/meta/etc if it doesn't exist

The Biggest Problem

You've reinvented term meta. There's already an API for this!!!!!

  • get_term_meta
  • add_term_meta
  • update_term_meta
  • delete_term_meta

This code is much more reliable and avoids several problems you didn't realise:

add_action( 'category_edit_form_fields', 'display_category_title_field' );
add_action( 'edit_category', 'save_category_title_field' );

function display_category_title_field( \WP_Term $cat ) : void {
    $title = get_term_meta( $cat->term_id, 'title', true );
    ?>
    <tr class="form-field">
        <th scope="row" valign="top">
            <label for="category-title"><?php esc_html_e( 'Title' ); ?></label>
        </th>
        <td>
            <input id="category-title" name="category-title"
            value="<?php echo esc_attr( $title ); ?>" />
        </td>
    </tr>
    <?php
}

function save_category_title_field( int $term_id ) : void {
    if ( ! isset( $_POST['category-title'] ) ) {
        return;
    }
    $title = sanitize_text_field( $_POST['category-title'] );
    update_term_meta( $term_id, 'title', $title );
}
  • when the category is deleted you aren't left with phantom options, term meta gets cleaned up
  • We're now sanitising our inputs
  • no more arrays and serialised data, it stores a plain string
  • when WP fetches a term it grabs all the term meta at the same time saving on queries
  • if you'd used this system with enough terms you would have made every page load very slow from auto-loading all those options
  • grabbing the value is much easier now get_term_meta( $cat_id, 'title', true );
  • The data is now accessible via WP CLI wp term meta commands
  • If you register the meta it can even show up in REST API requests for use with AJAX/JS

A final note, terms already have a title/name, and you've chosen super generic name for your fields/functions/etc. Prefix your values so they're unique to you, e.g. gurky_title instead of title. And maybe name them term titles rather than category titles? That way if you decide to use them for tags etc you don't have to rewrite half your code

3
  • Wow. Thanks a lot for this elaborate answer. I am new to this forum and cannot vote. I will have to examine this carefully.
    – gurky
    Mar 11, 2023 at 14:19
  • you're the question asker, you can mark it as accepted if it's correct! This is a Question Answer site rather than a forum so treat these as fully formed responses, not replies in a message thread ( if someone else write an answer and they get more votes then there answer will move up the page and mine down )
    – Tom J Nowell
    Mar 11, 2023 at 17:06
  • Thanks for the hint. It’s marked.
    – gurky
    Mar 11, 2023 at 18:30
0

Caution do not use this code. It’s bad practice. See comment below.

<?php
/*
    Plugin Name: test plugin
    Plugin URI: https://domain.ch/
    Description: test plugin
    Author: some author
    Author URI: https://domain.ch/
    Version: 1
*/

add_action( 'category_edit_form_fields', 'my_category_custom_fields' );
add_action( 'edit_category', 'save_my_category_custom_fields' );

function my_category_custom_fields( $cat ) {
// the $cat variable is a taxonomy term object with properties like $cat->name, $cat->term_id etc... 
$loc =  get_locale();
 if ($loc == 'es_ES'){
  $locX = 'de_DE';
 } else {
  $locX = 'es_ES';
 }

// we need to know the values of our existing entries if any
$cat_option = get_option( 'tim_term_'.$cat->term_id );

?>
<table class="form-table">
    <tr class="form-field">
        <th scope="row" valign="top"><label for="category-title"><?php _e("Some descriptive title"); ?></label>
        <td>
            <input style="display:inline-block;width:50%;height:34px" id="category-title" name="tim_term_[<?php echo $locX ?>]" 
              value="
              <?php 
              //echo changed according to the recommendation from the first answer
              if ( isset( $cat_option[ $locX ] ) ) echo esc_attr( __( $cat_option[ $locX ] ) ); 
              ?>" />
        </td>
    </tr>
</table>
<?php
}

function save_my_category_custom_fields(int $term_id) {
//changed condition according to the recommendation from the first answer
if ( !isset( $_POST['tim_term_'] ) ){
    return;
}   
    $post = $_POST['tim_term_'];
    //creating a key and value in $post array to store the $term_id
    $post['id'] = $term_id;
    //the variable $term_id is passed to the option name
    update_option('tim_term_'.$term_id, $post);

}

Check database:

option_name: tim-term-8
option_value: a:2:{s:5:"es_ES";s:13:"Categoría 01";s:2:"id";i:8;}

or for another category

option_name: tim-term-7
option_value: a:2:{s:5:"es_ES";s:7:"Archivo";s:2:"id";i:7;}

The id is part of the option_name. But as mentioned above this is bad practice.

This should be better. According to the comments and in order to prevent leading other greenhorns like me onto the road to perdition, i take the liberty to alter this answer. This processes no sericalized data, just a text string with a variable option name and including a function that cleans the database when the plugin is deleted:

add_action( 'category_edit_form_fields', 'my_category_custom_fields' );
add_action( 'edit_category', 'save_my_category_custom_fields' );


//function for category edit page
function my_category_custom_fields( $cat ) {

$category_meta = get_option( 'tim_term_'.$cat->term_id );

?>
<table class="form-table">
    <tr class="form-field">
        <th scope="row" valign="top"><label for="category-title"><?php _e("Title"); ?></label>
        <img src="<?php echo TESTURL.'/images/'. $flag; ?>"/></th>
        <td>
            <input style="display:inline-block;width:50%;height:34px" id="category-title" name="tim_term" value="<?php if ( isset( $category_meta ) ) echo esc_attr( $category_meta ); ?>" />
        </td>
    </tr>
</table>
<?php
}

function save_my_category_custom_fields(int $term_id) {
if ( !isset( $_POST['tim_term'] ) ){
    return;
}   
    
    update_option('tim_term_'.$term_id, $_POST['tim_term']);

}

//delete all options when plugin is deleted
function myplugin_activation_callback(){
register_uninstall_hook( __FILE__, 'myplugin_uninstall_callback'    );
}
register_activation_hook( __FILE__, 'myplugin_activation_callback' );

function myplugin_uninstall_callback(){

//Perform your uninstall operations here 
//delete category options
$cat_args=array(
  'orderby' => 'name',
  'order' => 'ASC',
  'hide_empty'   => 1,
  'exclude' => 1// desire id
   );
$categories=get_categories($cat_args);

foreach($categories as $category) { 
    delete_option('tim_term_'.$category->term_id);
}

}

Sorry for the length of this answer.

2
  • 1
    note that this is still passing dynamic data though __() which is very bad, and you should avoid storing serialised data in the database, it makes it much harder to run queries and opens you up to object injection and code execution attacks. If you must store structured data in the DB as a single value please use wp_json_encode and json_decode to limit the changes. You should also add a deletion hook to perform cleanup or your DB will slowly fill up with orphaned term options
    – Tom J Nowell
    Mar 12, 2023 at 13:33
  • Thanks a lot for your input. I’ll try to adapt your recommendations and keep learning to do it right.
    – gurky
    Mar 12, 2023 at 22:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.