ClassicPress Plugin Development: Load Options with Defaults

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

Over the last couple of posts, I’ve taken a look at saving and loading options and how to load options with defaults. The defaults in the last post was a single dimension array, but you can also do the same with multi dimensional arrays using a ustom recursive parse of the arrays.

The below is an example of loading options with multi dimensional defaults from my Widget Announcements plugin:

/**
 * Get options including defaults.
 *
 * @since 1.1.0
 *
 */
function azrcrv_wa_get_option($option_name){
 
	$defaults = array(
						'widget' => array(
											'width' => 300,
											'height' => 300,
										),
						'to-twitter' => array(
												'integrate' => 0,
												'tweet' => 0,
												'retweet' => 0,
												'retweet-prefix' => 'ICYMI:',
												'tweet-format' => '%t %h',
												'tweet-time' => '10:00',
												'retweet-time' => '16:00',
												'use-featured-image' => 1,
											),
						'toggle-showhide' => array(
												'integrate' => 0,
											),
					);

	$options = get_option($option_name, $defaults);

	$options = azrcrv_wa_recursive_parse_args($options, $defaults);

	return $options;

}

/**
 * Recursively parse options to merge with defaults.
 *
 * @since 1.1.0
 *
 */
function azrcrv_wa_recursive_parse_args( $args, $defaults ) {
	$new_args = (array) $defaults;

	foreach ( $args as $key => $value ) {
		if ( is_array( $value ) && isset( $new_args[ $key ] ) ) {
			$new_args[ $key ] = azrcrv_wa_recursive_parse_args( $value, $new_args[ $key ] );
		}
		else {
			$new_args[ $key ] = $value;
		}
	}

	return $new_args;
}

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Load Options with Defaults

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

In the last post I covered saving and loading options in a ClassicPress plugin. When you create a plugin with options you will want to provide defaults to be used before the user makes any changes to the settings; this both allows for basic operation of the plugin and avoids unset option errors.

The get_option function does allow for defaults to be passed, but this will only work if there are no options; it will not work effectively if new options are added to the plugin. This can be handled using the wp_parse_args function which merges user defined arguments into defaults array.

wp_parse_args( string|array|object $args, array $defaults = array() )

Parameters

$args (string|array|object) (Required) Value to merge with $defaults. $defaults (array) (Optional) Array that serves as the defaults. Default value: array()

Return

(array) Merged user defined values with defaults.

The below is an example of loading options with defaults from my Comment Validator plugin:

/**
 * Get options including defaults.
 *
 * @since 1.2.0
 *
 */
function azrcrv_cv_get_option($option_name){
 
	$defaults = array(
						'min_length' => 10,
						'max_length' => 500,
						'mod_length' => 250,
						'prevent_unreg_using_reg_name' => 1,
						'use_network' => 1,
					);

	$options = get_option($option_name, $defaults);

	$options = wp_parse_args($options, $defaults);

	return $options;

}

The above example works when the default options is single level. If the options are multilevel, these need to be handled differently; I will cover this in the next post in this series.

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Load and Save Options

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

When developing a plugin, most of them will have settings which need to be saved ad recalled. There are functions available in ClassicPress which you can use to do this:

    get_option

    update_option

If your plugin contains multiple options, then best practice would be to store these in an array within one option rather than each option stored individually.

The get_option is used to load options from the database:

get_option( string $option, mixed $default = false )

Parameters

$option (string) (Required) Name of option to retrieve. Expected to not be SQL-escaped. $default (mixed) (Optional) Default value to return if the option does not exist. Default value: false

Return

(mixed) Value set for the option.

The below is an example of loading options from my SMTP plugin:

$options = get_option( 'azrcrv-smtp' );

The update_option function is used to save options:

update_option( string $option, mixed $value, string|bool $autoload = null )

Parameters

$option (string) (Required) Option name. Expected to not be SQL-escaped. $value (mixed) (Required) Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped. $autoload (string|bool) (Optional) Whether to load the option when ClassicPress starts up. For existing options, $autoload can only be updated using update_option() if $value is also changed. Accepts 'yes'|true to enable or 'no'|false to disable. For non-existent options, the default value is 'yes'. Default value: null

Return

(bool) False if value was not updated and true if value was updated.

The below is an example of saving options from my SMTP plugin:

update_option( 'azrcrv-smtp', $options );

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Create Submenu for a Custom Post Type

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

If you’ve created a plugin with a custom post type you can add additional submenus to the custom post types top level menu. I typically use this to add the options page of the plugin. This is basically done as a variation on the theme of adding a submenu to custom top level menu.

This is an example from my From Twitter plugin which has a custom post type called Tweet; the key difference between this and adding a submenu to the Settings menu is the $tag which is the first argument.

/**
 * Add to menu.
 *
 * @since 1.0.0
 *
 */
function azrcrv_ft_create_admin_menu(){
	
	add_submenu_page(
						'edit.php?post_type=tweet'
						,esc_html__('From Twitter Settings', 'from-twitter')
						,esc_html__('Settings', 'from-twitter')
						,'manage_options'
						,'azrcrv-ft'
						,'azrcrv_ft_display_options'
					);
	
}

You can find the tag to use by hovering the mouse over the top level menu of the custom post type.

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Create Custom Post Type

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

ClassicPress is intended to be extended and customised and has numerous ways this can be done. Shortcodes are a common way of doing this, but another way is to create a custom post type.

A post type in ClassicPress is a type of content. A single item is called a post, but this is also the name of a standard post type called posts which are used in the blog part of ClassicPress.

The default post types which ship with ClassicPress are:

  • Posts
  • Pages
  • Attachments
  • Revisions
  • Navigation Menus
  • Custom CSS
  • Changesets

You would a custom post type for an additional type of content which does not fit within the existing post types. For example, if you wanted to create a archive of tweets from Twitter, as I did with my From Twitter plugin, create a Tweet custom post type.

Post types can support any number of built-in core features such as meta boxes, custom fields, post thumbnails, post statuses, comments, and more. See the $supports argument, below, for a complete list of supported features.

Custom post types are created usig the register_post_type function:

register_post_type(string $post_type, array|string $args = array())

Parameters

$post_type (string) (Required) Post type key. Must not exceed 20 characters and may only contain lowercase alphanumeric characters, dashes, and underscores. See sanitize_key(). $args (array|string) (Optional) Array or string of arguments for registering a post type.
  • 'label' (string) Name of the post type shown in the menu. Usually plural. Default is value of $labels['name'].
  • 'labels' (string[]) An array of labels for this post type. If not set, post labels are inherited for non-hierarchical types and page labels for hierarchical ones. See get_post_type_labels() for a full list of supported labels.
  • 'description' (string) A short descriptive summary of what the post type is.
  • 'public' (bool) Whether a post type is intended for use publicly either via the admin interface or by front-end users. While the default settings of $exclude_from_search, $publicly_queryable, $show_ui, and $show_in_nav_menus are inherited from public, each does not rely on this relationship and controls a very specific intention. Default false.
  • 'hierarchical' (bool) Whether the post type is hierarchical (e.g. page). Default false.
  • 'exclude_from_search' (bool) Whether to exclude posts with this post type from front end search results. Default is the opposite value of $public.
  • 'publicly_queryable' (bool) Whether queries can be performed on the front end for the post type as part of parse_request(). Endpoints would include:
    • ?post_type={post_type_key}
    • ?{post_type_key}={single_post_slug}
    • ?{post_type_query_var}={single_post_slug}
    If not set, the default is inherited from $public.
  • 'show_ui' (bool) Whether to generate and allow a UI for managing this post type in the admin. Default is value of $public.
  • 'show_in_menu' (bool|string) Where to show the post type in the admin menu. To work, $show_ui must be true. If true, the post type is shown in its own top level menu. If false, no menu is shown. If a string of an existing top level menu (eg. 'tools.php' or 'edit.php?post_type=page'), the post type will be placed as a sub-menu of that. Default is value of $show_ui.
  • 'show_in_nav_menus' (bool) Makes this post type available for selection in navigation menus. Default is value of $public.
  • 'show_in_admin_bar' (bool) Makes this post type available via the admin bar. Default is value of $show_in_menu.
  • 'show_in_rest' (bool) Whether to include the post type in the REST API. Set this to true for the post type to be available in the block editor.
  • 'rest_base' (string) To change the base url of REST API route. Default is $post_type.
  • 'rest_controller_class' (string) REST API Controller class name. Default is 'WP_REST_Posts_Controller'.
  • 'menu_position' (int) The position in the menu order the post type should appear. To work, $show_in_menu must be true. Default null (at the bottom).
  • 'menu_icon' (string) The url to the icon to be used for this menu. Pass a base64-encoded SVG using a data URI, which will be colored to match the color scheme -- this should begin with 'data:image/svg+xml;base64,'. Pass the name of a Dashicons helper class to use a font icon, e.g. 'dashicons-chart-pie'. Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS. Defaults to use the posts icon.
  • 'capability_type' (string) The string to use to build the read, edit, and delete capabilities. May be passed as an array to allow for alternative plurals when using this argument as a base to construct the capabilities, e.g. array('story', 'stories'). Default 'post'.
  • 'capabilities' (string[]) Array of capabilities for this post type. $capability_type is used as a base to construct capabilities by default. See get_post_type_capabilities().
  • 'map_meta_cap' (bool) Whether to use the internal default meta capability handling. Default false.
  • 'supports' (array) Core feature(s) the post type supports. Serves as an alias for calling add_post_type_support() directly. Core features include 'title', 'editor', 'comments', 'revisions', 'trackbacks', 'author', 'excerpt', 'page-attributes', 'thumbnail', 'custom-fields', and 'post-formats'. Additionally, the 'revisions' feature dictates whether the post type will store revisions, and the 'comments' feature dictates whether the comments count will show on the edit screen. A feature can also be specified as an array of arguments to provide additional information about supporting that feature. Example: array('my_feature', array('field' => 'value')). Default is an array containing 'title' and 'editor'.
  • 'register_meta_box_cb' (callable) Provide a callback function that sets up the meta boxes for the edit form. Do remove_meta_box() and add_meta_box() calls in the callback. Default null.
  • 'taxonomies' (string[]) An array of taxonomy identifiers that will be registered for the post type. Taxonomies can be registered later with register_taxonomy() or register_taxonomy_for_object_type().
  • 'has_archive' (bool|string) Whether there should be post type archives, or if a string, the archive slug to use. Will generate the proper rewrite rules if $rewrite is enabled. Default false.
  • 'rewrite' (bool|array) Triggers the handling of rewrites for this post type. To prevent rewrite, set to false. Defaults to true, using $post_type as slug. To specify rewrite rules, an array can be passed with any of these keys:
    • 'slug' (string) Customize the permastruct slug. Defaults to $post_type key.
    • 'with_front' (bool) Whether the permastruct should be prepended with WP_Rewrite::$front. Default true.
    • 'feeds' (bool) Whether the feed permastruct should be built for this post type. Default is value of $has_archive.
    • 'pages' (bool) Whether the permastruct should provide for pagination. Default true.
    • 'ep_mask' (int) Endpoint mask to assign. If not specified and permalink_epmask is set, inherits from $permalink_epmask. If not specified and permalink_epmask is not set, defaults to EP_PERMALINK.
  • 'query_var' (string|bool) Sets the query_var key for this post type. Defaults to $post_type key. If false, a post type cannot be loaded at ?{query_var}={post_slug}. If specified as a string, the query ?{query_var_string}={post_slug} will be valid.
  • 'can_export' (bool) Whether to allow this post type to be exported. Default true.
  • 'delete_with_user' (bool) Whether to delete posts of this type when deleting a user.
    • If true, posts of this type belonging to the user will be moved to Trash when the user is deleted.
    • If false, posts of this type belonging to the user will *not* be trashed or deleted.
    • If not set (the default), posts are trashed if post type supports the 'author' feature. Otherwise posts are not trashed or deleted. Default null.
  • 'template' (array) Array of blocks to use as the default initial state for an editor session. Each item should be an array containing block name and optional attributes.
  • 'template_lock' (string|false) Whether the block template should be locked if $template is set.
    • If set to 'all', the user is unable to insert new blocks, move existing blocks and delete blocks.
    • If set to 'insert', the user is able to move existing blocks but is unable to insert new blocks and delete blocks. Default false.
  • '_builtin' (bool) FOR INTERNAL USE ONLY! True if this post type is a native or "built-in" post_type. Default false.
  • '_edit_link' (string) FOR INTERNAL USE ONLY! URL segment to use for edit link of this post type. Default 'post.php?post=%d'. Default value: array()

Return

(WP_Post_Type|WP_Error) The registered post type object on success, WP_Error object on failure.

Below is an example of the registration of a custom post type in my Call-out Boxes plugin; this custom post type is not public as it only does output within the plugins shortcode and does not allow for searching:

add_action('init', 'azrcrv_cob_create_custom_post_type');

/**
 * Create custom snippet post type.
 *
 * @since 1.0.0
 *
 */
function azrcrv_cob_create_custom_post_type(){
	register_post_type('call-out-box',
		array(
				'labels' => array(
									'name' => __('Templates', 'call-out-boxes'),
									'singular_name' => __('Template', 'call-out-boxes'),
									'menu_name' => __( 'Call-out Boxes', 'call-out-boxes' ),
									'name_admin_bar' => __( 'Call-out Box Template', 'call-out-boxes' ),
									'all_items' => __('All Templates', 'call-out-boxes'),
									'add_new' => __('Add New Template', 'call-out-boxes'),
									'add_new_item' => __('Add New Call-out Box Template', 'call-out-boxes'),
									'edit' => __('Edit Template', 'call-out-boxes'),
									'edit_item' => __('Edit Call-out Box Template', 'call-out-boxes'),
									'new_item' => __('New Call-out Box Template', 'call-out-boxes'),
									'view' => __('View Template', 'call-out-boxes'),
									'view_item' => __('View Call-out Box Template', 'call-out-boxes'),
									'search_items' => __('Search Call-out Box Templates', 'call-out-boxes'),
									'not_found' => __('No Call-out Box Templates found', 'call-out-boxes'),
									'not_found_in_trash' => __('No Call-out Box Templates found in Trash', 'call-out-boxes'),
									'parent' => __('Parent Call-out Box', 'call-out-boxes')
								),
			'public' => false,
			'exclude_from_search' => true,
			'publicly_queryable' => false,
			'menu_position' => 50,
			'supports' => array('title'),
			'taxonomies' => array(''),
			'menu_icon' => 'dashicons-testimonial',
			'has_archive' => false,
			'show_ui' => true,
			'show_in_menu' => true,
			'show_in_admin_bar' => true,
			'show_in_nav_menus' => false,
			'show_in_rest' => false,
		)
	);
}

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: ClassicPress Plugin Development: Rename First Sublevel Menu of a Custom Top Level Menu

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

In the last post I showed how to create a custom top level menu and mentioned it creted both the top level menu and a sublevel menu of the same name.

As the below example, extracted from my To Twitter plugin which adds a submenu to the existing azrcrv-tt menu with a name of Settings. As the first, $parent_slug, and fifth, $menu_slug parameters match the top level menu the add_submenu_page function renames the default first submenu entry:

add_action('admin_menu', 'azrcrv_tt_create_admin_menu');

/**
 * Add to menu.
 *
 * @since 1.0.0
 *
 */
function azrcrv_tt_create_admin_menu(){
    
	add_submenu_page(
				'azrcrv-tt'
				,__('Settings', 'to-twitter')
				,__('Settings', 'to-twitter')
				,'manage_options'
				,'azrcrv-tt'
				,'azrcrv_t_display_options');
				
}

To rename a custom top level menu on a network admin dashboard, change the admin_menu tag in the add_action function call to network_admin_menu.

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Create Submenu on Custom Top Level Menu

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

Once you have added a custom top level menu for your plugin, you can add submenu items. This is done using the add_submenu_page function:

add_submenu_page(string $parent_slug, string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '', int $position = null)

Click to show/hide

$parent_slug (string) (Required) The slug name for the parent menu (or the file name of a standard WordPress admin page). $page_title (string) (Required) The text to be displayed in the title tags of the page when the menu is selected. $menu_title (string) (Required) The text to be used for the menu. $capability (string) (Required) The capability required for this menu to be displayed to the user. $menu_slug (string) (Required) The slug name to refer to this menu by. Should be unique for this menu and only include lowercase alphanumeric, dashes, and underscores characters to be compatible with sanitize_key(). $function (callable) (Optional) The function to be called to output the content for this page. Default value: '' $position (int) (Optional) The position in the menu order this item should appear. Default value: null

The below example is extracted from my To Twitter plugin which adds a submenu to the azrcrv-m menu item:

add_action('admin_menu', 'azrcrv_tt_create_admin_menu');

/**
 * Add to menu.
 *
 * @since 1.0.0
 *
 */
function azrcrv_tt_create_admin_menu(){
				
	add_submenu_page(
				'azrcrv-tt'
				,__('Send Tweet', 'to-twitter')
				,__('Send Tweet', 'to-twitter')
				,'manage_options'
				,'azrcrv-tt-smt'
				,'azrcrv_tt_display_send_manual_tweet');
				
}

This will add a second sublevel menu to the custom top level menu which takes the user to a different options page.

To add a custom top level menu to a network admin dashboard, change the admin_menu tag in the add_action function call to network_admin_menu.

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Create Custom Top Level Menu

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

While it is most common to add an option spage for a plugin to the Settings or Security top level menu, it is possible to create a custom top level menu.

A top level menu can be created using the add_menu_page function:

add_menu_page(string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '', string $icon_url = '', int $position = null)

Click to show/hide

$page_title (string) (Required) The text to be displayed in the title tags of the page when the menu is selected. $menu_title (string) (Required) The text to be used for the menu. $capability (string) (Required) The capability required for this menu to be displayed to the user. $menu_slug (string) (Required) The slug name to refer to this menu by. Should be unique for this menu page and only include lowercase alphanumeric, dashes, and underscores characters to be compatible with sanitize_key(). $function (callable) (Optional) The function to be called to output the content for this page. Default value: '' $icon_url (string) (Optional) The URL to the icon to be used for this menu.
  • Pass a base64-encoded SVG using a data URI, which will be coloured to match the color scheme. This should begin with 'data:image/svg+xml;base64,'.
  • Pass the name of a Dashicons helper class to use a font icon, e.g. 'dashicons-chart-pie'.
  • Pass 'none' to leave div.wp-menu-image empty so an icon can be added via CSS.
  • Default value: ''

Below is an example of a custom top level menu from my To Twitter plugin:

add_action('admin_menu', 'azrcrv_tt_create_admin_menu');

/**
 * Add to menu.
 *
 * @since 1.0.0
 *
 */
function azrcrv_tt_create_admin_menu(){

    add_menu_page(
				__('To Twitter', 'to-twitter')
				,__('To Twitter','to-twitter')
				,'manage_options'
				,'azrcrv-tt'
				,'azrcrv_tt_display_options'
				,'dashicons-twitter'
				, 50);
				
}

The top level menu automatically has a sublevel menu of the same name added; I’ll show how to rename this in the next post of this series.

To add a custom top level menu to a network admin dashboard, change the admin_menu tag in the add_action function call to network_admin_menu.

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Add Plugin Options Page to Security Main Menu

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

When developing a plugin, it is usual to create an options page to allow users to configure the plugin. The most common way of making the plugin options page available to users is to add it to the Settings menu in the admin dashboard; however, ClassicPress has a Security menu available which allows security plugins to be separated from the other settings on a site. This Security menu does not exist in WordPress so if you’re writing a plugin to be compatible with both ClassicPress and WordPress you will need to manage this when adding the options page.

This is done using the add_security_page function available with ClassicPress.

add_security_page(string $page_title, string $menu_title, string $menu_slug, callable $function = '')

Parameters

$page_title (string) (Required) The text to be displayed in the title tags of the page when the menu is selected. $menu_title (string) (Required) The text to be used for the menu. $menu_slug (string) (Required) The slug name to refer to this menu by (should be unique for this menu); must match an active plugin or mu-plugin slug.. $function (callable) (Optional) The function to be called to output the content for this page. Default value: ''

Return

(string|false) The resulting page's hook_suffix, or false if the user does not have the capability required.

The function above is made accessible using the add_action function along with a admin_menu tag.

The below is an example, including check for the security menu being available, of an options page using my vendor prefix of azrcrv and a plugin identifer of XXXX:

add_action('admin_menu', 'azrcrv_XXXX_add_options_page');

function azrcrv_XXXX_add_options_page() {
	if (function_exists('add_security_page')){
		add_security_page( 
			esc_html__('XXXX Options', 'text-domain'),
			esc_html__('XXXX', 'text-domain'),
			dirname(plugin_basename(__FILE__ )),
			'azrcrv_XXXX_display_options_page'
		);
	}else{
		// add options in WordPress compatible way; possibly using the add_options_page function.
	}
}

If the menu being added is for a network, rather than individual site, the $tag would be network_admin_menu instead of admin_menu.

When an options page is added to the Security menu, a plugin action link is automatically added:

Security plugin action link example

Click to show/hide the ClassicPress Plugin Development Series Index

ClassicPress Plugin Development: Add Plugin Options Page to Settings Main Menu

ClassicPress PluginsThis post is part of the ClassicPress Plugin Development series in which I am going to look at both best practice for developing plugins and how I approach some requirements as well as some of the functions I commonly use.

When developing a plugin, it is usual to create an options page to allow users to configure the plugin. The most common way of making the plugin options page available to users is to add it to the Settings menu in the admin dashboard.

This is done using the add_options_page function available with ClassicPress.

add_options_page(string $page_title, string $menu_title, string $capability, string $menu_slug, callable $function = '', int $position = null)

Parameters

$page_title (string) (Required) The text to be displayed in the title tags of the page when the menu is selected. $menu_title (string) (Required) The text to be used for the menu. $capability (string) (Required) The capability required for this menu to be displayed to the user. $menu_slug (string) (Required) The slug name to refer to this menu by (should be unique for this menu). $function (callable) (Optional) The function to be called to output the content for this page. Default value: '' $position (int) (Optional) The position in the menu order this item should appear. Default value: null

Return

(string|false) The resulting page's hook_suffix, or false if the user does not have the capability required.

The function above is made accessible using the add_action function along with a admin_menu tag.

The below is an example of an options page using my vendor prefix of azrcrv and a plugin identifer of XXXX:

add_action('admin_menu', 'azrcrv_XXXX_add_options_page');

function azrcrv_XXXX_add_options_page() {
    add_options_page( 
        esc_html__('XXXX Options', 'text-domain'),
        esc_html__('XXXX', 'text-domain'),
        'manage_options',
        dirname(plugin_basename(__FILE__ )),
        'azrcrv_XXXX_display_options_page'
    );
}

If the menu being added is for a network, rather than individual site, the $tag would be network_admin_menu instead of admin_menu.

Click to show/hide the ClassicPress Plugin Development Series Index