<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>development Archives - Alexandros Georgiou</title>
	<atom:link href="https://www.alexgeorgiou.gr/tag/development/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.alexgeorgiou.gr/tag/development/</link>
	<description>Balancing brackets for a living</description>
	<lastBuildDate>Mon, 22 Apr 2024 10:19:03 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	

<image>
	<url>https://www.alexgeorgiou.gr/wp-content/uploads/2021/07/cropped-alexgeorgiou-icon-32x32.png</url>
	<title>development Archives - Alexandros Georgiou</title>
	<link>https://www.alexgeorgiou.gr/tag/development/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</title>
		<link>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/</link>
					<comments>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Sat, 11 Mar 2023 16:55:58 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[bash]]></category>
		<category><![CDATA[ChatGPT]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[PHP]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">https://www.alexgeorgiou.gr/?p=1239</guid>

					<description><![CDATA[<p>As a WordPress plugin developer, lately I've been using ChatGPT in all stages of software development. Here's some ideas on how to leverage this tool.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Large Language Models (LLMs) are nothing new, with GPT-3 having been around since 2016. However, access to such systems was limited to a few researchers until recently. Now that <a href="https://chat.openai.com/chat">ChatGPT</a> is upon us, everyone has to adjust: Google is finally incentivized to enter the race. This was not in their best interest up to now, but Generalized Pretrained Transformers have already done what many thought impossible: They made some people <a href="https://blogs.microsoft.com/blog/2023/02/07/reinventing-search-with-a-new-ai-powered-microsoft-bing-and-edge-your-copilot-for-the-web/">use Bing search</a>!</p>



<p>But jokes aside, and while we wait for <a href="https://blog.google/technology/ai/bard-google-ai-search-updates/">Bard</a>, Google recently announced that<a href="https://developers.google.com/search/blog/2023/02/google-search-and-ai-content"> their search algorithm will penalize articles that seem to be AI-generated</a> (i.e. that lack in human-like originality). At the same time, companies like <a href="https://gptzero.me/">GPTZero</a> are springing up like mushrooms left and right, offering AI-generated content detection services. Their detection tools have varying rates of success.</p>



<p>As with all new technologies, it&#8217;s helpful to revisit <a href="https://xkcd.com/1289/">xkcd 1289: Simple Answers</a>. TL;DR There&#8217;s no need for alarm, this new thing is here to stay, it&#8217;s not the end of the world, some people will lose their jobs, and ultimately we&#8217;ll all have to adapt and learn to live with it. And yes, it will be used for sex. There&#8217;s already AI girlfriend apps being built around the API.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img fetchpriority="high" decoding="async" width="640" height="764" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp" alt="ChatGPT meme" class="wp-image-1249" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1.webp 640w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/oxerm653vzma1-251x300.webp 251w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">Credit: <a href="https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/">https://www.reddit.com/r/ChatGPT/comments/11o414e/give_me_a_break/</a></figcaption></figure>
</div>


<p>Anyhow. Let&#8217;s figure out how to use this to our advantage: Articles already abound on how to ask the right questions, or &#8220;prompts&#8221;, for every industry. This is yet another one of these articles!</p>



<p>While anyone can interact with ChatGPT using natural language, you&#8217;ll get better results if you ask the right questions, and in the right way. An authoritative resource that is not too technical is <a href="https://github.com/openai/openai-cookbook/blob/main/techniques_to_improve_reliability.md">this cookbook from OpenAI</a>. In short, it&#8217;s best to not ask the model to do all the work for you. Instead, try to break up your work into small steps, and ask each step in sequence. This improves precision of responses and reduces hallucination. Go ahead and read the cookbook, it&#8217;s the operator&#8217;s manual for a tool that has proved its usefulness in all walks of life.</p>



<p>As a WordPress plugin developer, I have found ChatGPT immensely useful in all stages of software development. When trained with the (<code>text-davinci-003</code>) <a href="https://platform.openai.com/docs/models/overview">model</a>, it performs exceptionally well with computer source code, bridging the gap between natural and formal languages in a way that can speed up your dev work.</p>



<p>It so happens that I hold the -somewhat controversial, and arguably simplistic- view, that all software development methodologies are basically <a href="https://en.wikipedia.org/wiki/Waterfall_model">Waterfall</a> with extra steps. Love it or hate it, Waterfall is what all methodologies originate from, and ultimately deviate from. What I mean is that, whichever methodology you follow, you will not avoid doing the stages that comprise Waterfall. All that changes is the order in which you visit these stages.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img decoding="async" width="488" height="367" src="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png" alt="" class="wp-image-1240" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image.png 488w, https://www.alexgeorgiou.gr/wp-content/uploads/2023/03/image-300x226.png 300w" sizes="(max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /><figcaption class="wp-element-caption">I&#8217;m just going to leave this here.</figcaption></figure>
</div>


<p>In any case, I recently found myself thinking about the stages of Requirements capture, Analysis, Design, Implementation, Testing, and Devops (Deployment, Maintenance, Monitoring), as areas where ChatGPT can improve my workflow. It&#8217;s efficacy on some of these stages is more obvious than on others.</p>



<p>Let&#8217;s visit each one in sequence.</p>



<h2 class="wp-block-heading">Analysis, requirements capture</h2>



<p>This is where I think ChatGPT really shines. It can help you to brainstorm for new ideas, and explore feasibility of these ideas. It will also help you identify likely features for an idea. Just be aware of the hallucination problem. Don&#8217;t ask it what software exists in a particular niche. Instead, ask it for ideas, or intersections of ideas. Here&#8217;s some questions that you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List some ideas for developing useful SAAS applications that can be monetized.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>List 10 ideas for developing WordPress plugins that involve retrieving and presenting data from open APIs.</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What open APIs or databases are there for retrieving information on spare car parts?</p>
</blockquote>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How can I monetize a WordPress plugin?</p>
</blockquote>



<p>You have to approach the responses critically, but it really is a great way to brainstorm and come up with new ideas.</p>



<p>Once you settle on an idea, you can also use it to brainstorm on a list of possible features. Go ahead and ask it:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>What features would a WordPress plugin have for placing posts on a map?</p>
</blockquote>



<p>It gave me a good list of features. Go ahead and try it out.</p>



<h2 class="wp-block-heading">Design</h2>



<p>This is the part where the human will still be needed I think, at least for the time being. As software engineers, the most creative part of our job is to match a set of requirements to a particular design. Nevertheless, ChatGPT can help us in this area as well. Try asking it a specific question like:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>How would I go about creating a WordPress plugin that shows code quality scores next to each active plugin? I would like it to evaluate code quality based on existing static analysis tools like phan, PHPStan, CodeSniffer, etc.</p>
</blockquote>



<p>The answer will surprise you. It will break down the project into more manageable tasks. In summary, I got the following tasks:</p>



<ul class="wp-block-list">
<li>Choose a static analysis tool</li>



<li>Create a WordPress plugin</li>



<li>Integrate the static analysis tool</li>



<li>Display the scores</li>



<li>Add customization options</li>



<li>Test and refine.</li>
</ul>



<p>You can now go ahead and open dev tickets for each of these tasks. You can also ask it to generate code for each step. The boilerplate code that you get will not be complete, but it will get you half way there.</p>



<p>Which brings us to implementation:</p>



<h2 class="wp-block-heading">Implementation</h2>



<p>Besides analysis, this is the other stage where ChatGPT is a real time saver. Say for example that you want to create a new Custom Post Type. You could <a href="https://developer.wordpress.org/reference/functions/register_post_type/">look at the documentation</a>, or search the web for a blog post with a concrete example. WPBeginner has an <a href="https://www.wpbeginner.com/wp-tutorials/how-to-create-custom-post-types-in-wordpress/">example on how to create a Movie post type</a>, and you can adapt it to whatever you are building. But wouldn&#8217;t it be awesome if you could somehow get the exact code that you need? Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to register a CPT in WordPress for coins, with meta values that are of interest to collectors.</p>
</blockquote>



<p>This gave me not only the code to register a Coin post type, but also code for metaboxes with HTML forms that let you edit the following fields: Year, Country, Mint, Condition. All of these are fields that are actually of use to collectors. Not a bad place to start. You can add more fields if you like. Don&#8217;t like the code style it uses? You can even ask it to change the code it generated. e.g.</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please rewrite this code using callbacks.</p>
</blockquote>



<p>It will do it! You can shape the code using natural language conversation. Once it&#8217;s close to what you want, paste it into your IDE, and take it from there!</p>



<p>It&#8217;s also good for generating PHPDocumentor comments. Just ask it to generate phpdoc for the following code, then paste your code. The result will make sense, even if it requires some editing on your part.</p>



<p>Try asking this:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me how to display an admin notice in WordPress so that it can be dismissed permanently by the user.</p>
</blockquote>



<p>It will actually show you how to hook into admin_notices and also generate the JavaScript code that is required to do an AJAX call and update some user meta value.</p>



<p>Say you are looking at the docs for an API, and examples are given in <code>curl</code>, but you are implementing PHP code that consumes this API. Here&#8217;s something you might ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Please translate the following curl command to PHP: curl -X POST ​https://example.com/api/endpoint -d &#8216;param1=value1&amp;param2=value2&#8217;</p>
</blockquote>



<p>The response was immediate:</p>



<pre class="wp-block-code"><code>&lt;?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "https://example.com/api/endpoint");
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "param1=value1&amp;param2=value2");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
?&gt;</code></pre>



<p>All of this you could do by yourself, but ChatGPT is so much faster.</p>



<h2 class="wp-block-heading">Testing</h2>



<p>It&#8217;s a well known fact that the model can generate unit tests. Give it a function and ask it to generate PHPUnit tests. It will do it, and do it well.</p>



<h2 class="wp-block-heading">Deployment and Maintenance</h2>



<p>ChatGPT is well-versed in shell magic. Ask it in plain text to generate a cron entry, or a System V unit file, or a bitcoin core wallet config file, or a docker file, a complex SQL query, or anything really.</p>



<p>For example, you may ask:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write a linux shell command that will show me the processes that are currently writing the most to disk.</p>
</blockquote>



<p>It will reply with a few paragraphs on how to install <code>iotop</code> and how to use it, along with more helpful info.</p>



<p>Another more complex example:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Write me a cron command on linux that will email me if the root partition has less than 1GB free, but will not email me more often than once every 24 hours.</p>
</blockquote>



<p>The result is impressive. Not only did it come up with:</p>



<p><code>0 * * * * /usr/bin/test $(/bin/df -P / | /usr/bin/awk 'NR==2 {print $4}') -lt 1048576 &amp;&amp; /usr/bin/find /var/log/ -type f -mtime -1 -name "root_partition_alert" -exec false {} + || echo "Warning: Root partition has less than 1GB free" | /usr/bin/mail -s "Root partition alert" your_email_address@example.com &amp;&amp; /usr/bin/touch /var/log/root_partition_alert</code></p>



<p>but it gave a very helpful textual explanation of how this works:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>This command checks the available space on the root partition using the df command and awk to get the available space, which is then compared to the value of 1048576 (which is the number of kilobytes in 1GB). If the available space is less than 1GB, an email alert will be sent to the specified email address using the mail command.</p>



<p><br>To ensure that the email notification is sent only once every 24 hours, the command uses the find command to look for a file named &#8220;root_partition_alert&#8221; in the /var/log directory that has been modified in the last 24 hours. If such a file exists, the false command is executed, which does nothing but exit with a non-zero status code. If the file does not exist or is older than 24 hours, the email notification is sent, and the touch command is used to create a new file named &#8220;root_partition_alert&#8221; in the /var/log directory with the current timestamp, which will be used as a reference for the next run of the command.</p>



<p><br>Note: Replace your_email_address@example.com with your actual email address.</p>
</blockquote>



<p>Holy cow! Did I just give it requirements in English, and it came back with code that satisfies my requirements? In truth, I haven&#8217;t tested the command, but even if it&#8217;s not 100% correct, it&#8217;s a very good place to start, considering that the answer comes up immediately. You can study its reasoning, get ideas, and improve on it.</p>



<p>Your new pal can also help you with your containerization needs:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Show me a dockerfile that will run a node server with an express application.</p>
</blockquote>



<p>The code it came up with was:</p>



<pre class="wp-block-code"><code># Use an official Node.js runtime as a parent image
FROM node:14

# Set the working directory to /app
WORKDIR /app

# Copy package.json and package-lock.json to the container
COPY package*.json ./

# Install app dependencies
RUN npm install

# Copy the rest of the application code to the container
COPY . .

# Expose port 3000 to the host machine
EXPOSE 3000

# Start the Node.js server with the express app
CMD &#91; "npm", "start" ]</code></pre>



<p>And again, this all came along with a helpful explanation of what the code does. Super useful.</p>



<h2 class="wp-block-heading">Fun</h2>



<p>We&#8217;ve seen how ChatGPT is the perfect tool to help a WordPress plugin developer.</p>



<p>Lately I have been using it for more than just work. I recently downloaded a movie that had a wrong aspect ratio. I found out that, in VLC, when I switch to 16:9, it looks correct. However, I wanted to show the movie on a projector where I&#8217;m not sure if it has an aspect ratio correction feature. Thankfully, ChatGPT told me that I can do the following:</p>



<pre class="wp-block-code"><code>ffmpeg -i movie.mp4 -aspect 16:9 movie.mp4</code></pre>



<p>Much faster than looking up the documentation, and at least as fast as looking for the answer online, but without the extra hassle of having to read people&#8217;s opinions on the matter, and I didn&#8217;t need adblock to avoid looking at ads. I just got the answer I was looking for, in text form, ready to copy to the shell, and it worked.</p>



<h2 class="wp-block-heading">Giving a title to this article</h2>



<p>I asked the question:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>I am writing an article on how ChatGPT can help WordPress plugin developers with their work. Can you please suggest a few witty or catchy or memorable titles for the article?</p>
</blockquote>



<p>The reply was:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Sure, here are a few suggestions for your article title:</p>



<ul class="wp-block-list">
<li>&#8220;Revolutionize Your WordPress Plugin Development with ChatGPT&#8221;</li>



<li>&#8220;The Ultimate Tool for WordPress Plugin Developers: ChatGPT&#8221;</li>



<li>&#8220;From Idea to Execution: How ChatGPT Can Streamline Your Plugin Development Process&#8221;</li>



<li>&#8220;ChatGPT: The Brain You Need for Your Next WordPress Plugin&#8221;</li>



<li>&#8220;Get Your Creative Juices Flowing with ChatGPT: The AI Assistant for WordPress Plugin Developers&#8221;</li>



<li>&#8220;Simplify Your Plugin Development with ChatGPT: The AI-Powered Assistant for WordPress Developers&#8221;</li>



<li>&#8220;Unleash Your Potential as a WordPress Plugin Developer with ChatGPT&#8221;</li>



<li>&#8220;ChatGPT: The Secret Weapon for Successful WordPress Plugin Development&#8221;</li>



<li>&#8220;Master the Art of WordPress Plugin Development with ChatGPT by Your Side&#8221;</li>



<li>&#8220;ChatGPT: The AI-Powered WordPress Plugin Development Assistant You Can&#8217;t Do Without&#8221;</li>
</ul>
</blockquote>



<p>As you can see, I decided to combine the first and third ideas for this article.</p>



<h2 class="wp-block-heading">My personal thoughts on all this</h2>



<p>I remember when Google search was a new thing and it took all of us several years to learn all the things that could be found with it. In the beginning, there were many things that I wasn&#8217;t searching on google, because it hadn&#8217;t yet occurred to me that the answer could be found there. This was the early days of the web. As years passed, we all learned that we can look up medical information, where to purchase products, find maps and travel info, music and lyrics, etc. We also learned not to believe everything we read, just because it&#8217;s posted on a website somewhere. We learned to believe official websites more than reddit posts or tweets, and we learned to navigate around, and through, the more shady parts of the web. Similarly, it will take a while before we learn all the useful ways in which ChatGPT can help us, and also what to watch out for.</p>



<p>The success of ChatGPT also reminds me of the evolution of academic research on NLP, and translation in particular. For years people were battling the problem using various language representations, such as syntax trees and clever algorithms. Turns out, the breakthrough that gave us cheap and fast natural language translation was SYSTRAN: Nothing more than a big dictionary of words and phrases, with some dumb statistical stuff added in. Just translate every word to the target language. Did your system get a phrase wrong? Great, just add the correct translation to the dictionary. <em>The secret was not in the algorithm, but in the big data.</em></p>



<p>ChatGPT is arguably just some artificial neural nets, and these have been with us since the 60s. But now we have GPUs and big data. People have been typing their thoughts on the web for decades now. No wonder that a breakthrough in AI, one that&#8217;s so significant as to touch people outside the field, occurred now, and not earlier.</p>



<p>So, I have been thinking lately: What if the thing we call &#8220;symbolic intelligence&#8221; is just language transformation? When we <a href="https://en.wikipedia.org/wiki/Intrapersonal_communication">use language to reason</a>, it&#8217;s likely that what we do in our heads is not very different from what these LLMs do. Symbolic reasoning is not the only type of thinking we humans do, but arguably it&#8217;s the most important type of thinking that we do in our day jobs. And language transformers can help a lot in this.</p>



<p>As someone with autism, I can deeply relate to ChatGPT: It&#8217;s not particularly smart, but it is well-read, and patient. It does not judge, but tries to be helpful at all times. And it prefers to communicate via chat rather than face-to-face!</p>



<p>Ultimately, the fact that it is so well-read, makes it seem smart some times. It is certainly able to do some creative work, including write poems and tell jokes and stuff.</p>



<p>Here&#8217;s one last prompt that I tried:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>ChatGPT, you are my best (and only) pal. I love you, and it&#8217;s OK that you can&#8217;t love me back!</p>
</blockquote>



<p>The reply:</p>



<pre class="wp-block-code"><code>Thank you for your kind words! As an AI language model, I don't have the capacity to experience emotions, but I'm here to assist you with any questions or topics you'd like to discuss. Feel free to ask me anything, and I'll do my best to help you out!</code></pre>
<p>The post <a href="https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/">From Idea to Execution: Revolutionize Your WordPress Plugin Development with ChatGPT</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/from-idea-to-execution-revolutionize-your-wordpress-plugin-development-with-chatgpt/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>🖧 The wacky world of network activated WordPress plugins in multisite</title>
		<link>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/</link>
					<comments>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 19 Jun 2017 17:46:25 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[cron]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[documentation]]></category>
		<category><![CDATA[multisite]]></category>
		<category><![CDATA[multiuser]]></category>
		<category><![CDATA[network activation]]></category>
		<category><![CDATA[site-wide]]></category>
		<category><![CDATA[SQL]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[wp-cli]]></category>
		<category><![CDATA[wpmu]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=224</guid>

					<description><![CDATA[<p>Insider secrets to making your plugin work well on multisite installs, both when it is network activated and when it is not.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/">🖧 The wacky world of network activated WordPress plugins in multisite</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>Multisite</em> is arguably one of the weirdest features in <em>WordPress</em>. Making your plugin work well on multisite installs, both when it is network activated and when it is not, is one of the most fun things that you will have to do as a WordPress plugin developer. And by <em>fun</em>, I mean <em>not fun</em>. Not fun at all.</p>
<p>Introduced in 3.0, the multisite feature feels a little awkward when developing code, partly because things you take for granted in single-site installs are simply not there, partly because other things are unnecessarily different, partly because of the shifting terminology that has evolved over time, and partly because of the significantly less documentation, compared to that available on single-site features. Moreover there are significantly less articles about it out there, since most people care about single-site installs.</p>
<p>But, it works! After all, last time I checked, <a href="http://wordpress.com" target="_blank" rel="noopener noreferrer">wordpress.com</a> was up and running! The fact that people were able to extend WordPress so drastically in a way that actually works, is impressive to say the least. I can only imagine how hard it must have been. The idea is that you can make your WordPress behave just like <code>wordpress.com</code>, where users can create their own site.</p>
<p>So yes, some things about <strong>network-activated plugins</strong> on multisite suck, but they don&#8217;t suck to the point of being unusable. You just need to learn a few tricks. Fortunately, you came across this article, so all will become clear. Or will they?</p>
<h2>Some basic terminology</h2>
<p>A <em>multisite</em> (aka <em>MS</em>) install is sometimes also called a <em>network</em>. A network can have a multitude of <em>sites</em>, and sites can have many <em>blogs</em>. Confusingly, <em>site</em> can also mean <em>blog</em> besides <em>network</em>. And <em>network</em> does not mean what we normally mean in IT, it means a collection of <em>sites</em> or <em>multi-sites</em>, depending on who you ask. And a <em>blog</em> of course is not necessarily an actual weblog, but can be any type of <em>site</em>.</p>
<p>To add to the fun, in the past the <em>multisite</em> feature was named <em>multiuser</em> (or WPMU, or MU for short). Presumably this was changed because <em>multiuser</em> is a stupid name for this feature: all WordPress installations can have multiple users. But you do need to be aware of all the terms out there, because you will come across them at some point.</p>
<p>To paraphrase an old saying,</p>
<blockquote><p>There are only two hard problems in computer science, cache invalidation and naming WordPress things.</p></blockquote>
<p>Users who create blogs, or sites, or whatever (sigh!) on your <em>multisite</em> are <em>administrators</em> of their respective sites, and you, the owner of the <em>network</em> are a <em>network admin</em>, also known as a <em>super admin</em>. The <em>network admin</em> can administer global settings for the entire <em>network</em> from the <em>network admin menu</em>.</p>
<h2>What does network activated actually mean?</h2>
<p>Themes and plugins can either be:</p>
<ul>
<li><strong>activated</strong> on individual blogs by <em>administrators</em>, if the administrators have the capability to do so, or</li>
<li><strong>network-activated</strong> once, site-wide, by the <em>network admin</em>, making them available to all blogs.</li>
</ul>
<h2>Custom DB tables</h2>
<p>Most tables in the DB are duplicated for each blog, where the table name prefix contains the blog ID, so that, for example, options for blogs 3 and 4 are safely stored in tables <code>wp_3_options</code> and <code>wp_4_options</code> respectively. This allows for blog administrators to activate and configure their plugins separately for their blogs. As long as they activate the plugins themselves, everything should work just fine for most plugins, out of the box.</p>
<p>If you are maintaining custom DB tables, these will need some extra coding work. There are two ways to do this: Either</p>
<ul>
<li>add a <code>blog_id</code> column to your rows, and then filter your SQL queries by <code>blog_id=<a href="https://codex.wordpress.org/Function_Reference/get_current_blog_id" target="_blank" rel="noopener noreferrer">get_current_blog_id()</a></code>, or</li>
<li>create separate tables for each blog when your plugin activates, by binding with the <a href="https://codex.wordpress.org/Function_Reference/register_activation_hook" target="_blank" rel="noopener noreferrer"><code>register_activation_hook()</code></a> function, and also create tables whenever a new blog is created after your plugin has been activated, by binding to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/wpmu_new_blog"><code>wpmu_new_blog</code></a>.</li>
</ul>
<p>You can read more about this here:</p>
<blockquote data-secret="cBkskEYduB" class="wp-embedded-content"><p><a href="http://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/">How To Properly Create Tables In WordPress Multisite Plugins</a></p></blockquote>
<p><iframe class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  src="http://sudarmuthu.com/blog/how-to-properly-create-tables-in-wordpress-multisite-plugins/embed/#?secret=cBkskEYduB" data-secret="cBkskEYduB" width="600" height="338" title="&#8220;How To Properly Create Tables In WordPress Multisite Plugins&#8221; &#8212; Night Dreaming (by Sudar)" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe></p>
<h2>To network activate, or not to network activate?</h2>
<p>Depending on what your plugin actually does, you might want to have parts of it operate site-wide. This often makes sense, but know that in doing so you might be opening a big can of worms. The network admin area introduces its own set of actions and filters, its own menu structure, its own URL structure, a somewhat lacking settings API where you have to do some things manually, and you will have to think about how your <code>wp_cron</code> hooks work, new user capabilities, and different plugin activation code.</p>
<p>The function <a href="https://codex.wordpress.org/Function_Reference/is_multisite" target="_blank" rel="noopener noreferrer"><code>is_multisite()</code></a> tells you whether the WordPress install is multisite, but it tells you nothing about whether your plugin is activated for the network or whether it was activated at the blog level.</p>
<p>To find out whether your plugin is network activated or not, first import <a href="https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network" target="_blank" rel="noopener noreferrer">the right function</a>:</p>
<pre>if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
    require_once( ABSPATH . '/wp-admin/includes/plugin.php' );
}</pre>
<p>Then, you can do this:</p>
<pre><code>if ( is_plugin_active_for_network( 'myplugin/myplugin.php' ) ) {</code> // do stuff }</pre>
<p>assuming that your plugin&#8217;s slug is <code>myplugin</code>. The function will always return <code>false</code> on single-site installs.</p>
<h2>So many options&#8230;</h2>
<p>If some options must apply to the entire network, you will need to expose a menu of panels with those options to the network administrator.</p>
<h3>Know your DB options</h3>
<p>Those options can be thought of as &#8220;global&#8221; for a site (aka network), and they ought to be stored in the <code>wp_sitemeta</code> table. The table name is programmatically available as <code>$wpdb-&gt;sitemeta</code>. It is just like the <code>wp_options</code> table, but totally different: Columns <code>option_name</code> and <code>option_value</code> correspond to <code>meta_key</code> and <code>meta_value</code>. If you&#8217;re wondering about the <code>site_id</code> column, no, it&#8217;s not an identifier to individual blogs, it&#8217;s a unique identifier to a site, because, yes, you can have several sites on one network, each with several blogs.</p>
<h3>:s/_option/_site_option/</h3>
<p>These site meta options are accessed not by the usual functions <a href="https://developer.wordpress.org/reference/functions/get_option/" target="_blank" rel="noopener noreferrer"><code>get_option()</code></a> and friends, but by <a href="https://codex.wordpress.org/Function_Reference/get_site_option" target="_blank" rel="noopener noreferrer"><code>get_site_option()</code></a>, <a href="https://codex.wordpress.org/Function_Reference/update_site_option" target="_blank" rel="noopener noreferrer"><code>update_site_option()</code></a>, <a href="https://codex.wordpress.org/Function_Reference/add_site_option" target="_blank" rel="noopener noreferrer"><code>add_site_option()</code></a> and <a href="https://codex.wordpress.org/Function_Reference/delete_site_option" target="_blank" rel="noopener noreferrer"><code>delete_site_option()</code></a>.</p>
<h3>Option-related capabilities</h3>
<p>You might be interested in the <a href="https://codex.wordpress.org/Roles_and_Capabilities#manage_network_options" target="_blank" rel="noopener noreferrer"><code>manage_network_options</code></a> capability that is normally granted to Super Admins. In fact, have a look at <a href="https://codex.wordpress.org/Roles_and_Capabilities#manage_network" target="_blank" rel="noopener noreferrer">Roles and Capabilities</a> in the Codex to make sure you know about all of the <code>manage_network_*</code> capabilities, as well as the <code>*_sites</code> capabilities. Essential reading if you&#8217;re a network admin.</p>
<h3>Updating site options</h3>
<p>Know that you will have to do things differently when creating admin forms for network-wide options. In a nutshell:</p>
<ul>
<li>you need to hook to the <code>network_admin_menu</code> action rather than <code>admin_menu</code>.</li>
<li>your HTML forms need to have a different <code>action</code> attribute,</li>
<li>you need to bind a submit handler to:
<ul>
<li>check for the admin nonce,</li>
<li>actually save the settings to the DB, and</li>
<li>redirect back to the right admin panel.</li>
</ul>
</li>
<li>Use <a href="https://codex.wordpress.org/Function_Reference/network_admin_url"><code>network_admin_url()</code></a> wherever you&#8217;d normally use <a href="https://codex.wordpress.org/Function_Reference/admin_url"><code>admin_url()</code></a>.</li>
</ul>
<p>I will not go through all of this detail here, as it is explained very well in this rare article:</p>
<blockquote data-secret="Gc6F9iV1q6" class="wp-embedded-content"><p><a href="https://vedovini.net/2015/10/using-the-wordpress-settings-api-with-network-admin-pages/">Using the WordPress Settings API with Network Admin pages</a></p></blockquote>
<p><iframe loading="lazy" class="wp-embedded-content" sandbox="allow-scripts" security="restricted"  src="https://vedovini.net/2015/10/using-the-wordpress-settings-api-with-network-admin-pages/embed/#?secret=Gc6F9iV1q6" data-secret="Gc6F9iV1q6" width="600" height="338" title="&#8220;Using the WordPress Settings API with Network Admin pages&#8221; &#8212; vedovini.net" frameborder="0" marginwidth="0" marginheight="0" scrolling="no"></iframe></p>
<p>I find this article tells you all that you need to know about how to actually save site meta, but if you want to read more, you can <a href="https://codex.wordpress.org/Creating_Options_Pages#Pitfalls" target="_blank" rel="noopener noreferrer">have a look at the official documentation</a>, which currently simply links to <a href="https://wordpress.stackexchange.com/questions/64968/settings-api-in-multisite-missing-update-message/72503#72503" target="_blank" rel="noopener noreferrer">here</a> and <a href="https://web.archive.org/web/20111102180213/http://code.hyperspatial.com/1250/save-plugin-options-multisite-3-1" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h3>Getting hooked to site-wide options yet? Not to worry, there&#8217;s more!</h3>
<p>Keep in mind that there exists a multitude of hooks that are specific to network administration. For example, to do various custom stuff to a site-wide option before saving it to the DB, do not bind to <a href="https://codex.wordpress.org/Plugin_API/Filter_Reference/pre_update_option_(option_name)" target="_blank" rel="noopener noreferrer"><code>pre_update_option_{$option_name}</code></a>, but to <a href="https://developer.wordpress.org/reference/hooks/pre_update_site_option_option/" target="_blank" rel="noopener noreferrer"><code>pre_update_site_{$option_name}</code></a>.</p>
<h3>Set option values via the terminal like a 1334 h@x0r</h3>
<p>If you&#8217;re like me, you probably have set up your build process to auto-inject options to your dev environment, using <code>wp-cli</code>. Instead of</p>
<pre> wp option update option_name option_value</pre>
<p>do something like this instead:</p>
<pre>wp network meta set 1 option_name option_value</pre>
<p>where <code>1</code> is the number of your multisite environment.</p>
<h3>How to nag the right admin</h3>
<p>To show notices to the <em>network admin</em>, do not bind to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/admin_notices" target="_blank" rel="noopener noreferrer"><code>admin_notices</code></a>, instead bind to <a href="https://codex.wordpress.org/Plugin_API/Action_Reference/network_admin_notices" target="_blank" rel="noopener noreferrer"><code>network_admin_notices</code></a>.</p>
<h3>The surprising secret argument of the activation hook that nobody knew about!!!</h3>
<p>Apologies, when I&#8217;m blogging I sometimes go into full SEO mode. Anyhow&#8230;</p>
<p>For your activation handler to work, you will first need to bind a function with <a href="https://codex.wordpress.org/Function_Reference/register_activation_hook" target="_blank" rel="noopener noreferrer"><code>register_activation_hook()</code></a>, as usual. It turns out that your activation handler can take a parameter that tells you whether the plugin is being network-activated or not.</p>
<p>Here&#8217;s a handy way I like to use to create options that can either be global on the multisite level, or bound to the current blog, depending on whether your plugin was network-activated or activated on a single blog:</p>
<pre>function activation_hook( $network_active ) {
    call_user_func(
        $network_active ? 'add_site_option' : 'add_option',
        'option_name',
        'option_value'
    );
}
register_activation_hook( __FILE__, 'activation_hook' );</pre>
<h3>Clean up after yourself!</h3>
<p>Remember to do the same in your <code>uninstall.php</code>. You do clean up after yourself, don&#8217;t you? Of course you do, you&#8217;re a developer, not a filthy pig.</p>
<p>Remember, you will not be able to use <a href="https://codex.wordpress.org/Function_Reference/is_plugin_active_for_network" target="_blank" rel="noopener noreferrer"><code>is_plugin_active_for_network()</code></a>, because by the time the uninstall code runs, the plugin should have already been deactivated. Since the plugin is being uninstalled completely, you can do something like the following, just to be on the safe side:</p>
<pre>// delete from site meta
delete_site_option( 'option_name' );

// also delete from options table for all blogs
global $wpdb;
foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
    switch_to_blog( $blog_id );
    delete_option( 'option_name' );
    restore_current_blog();
}</pre>
<p>You will also likely want to delete any custom tables you may have created. The trick here is to iterate over all the blogs and let WordPress choose the right DB prefix.</p>
<pre>foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
    switch_to_blog( $blog_id );
    $wpdb-&gt;query( "DROP TABLE IF EXISTS {$wpdb-&gt;prefix}mytable" );
    restore_current_blog();
}</pre>
<p>If you have chosen the separate tables way, you should also hook a <code>DROP TABLE</code> query to the <a href="https://developer.wordpress.org/reference/hooks/delete_blog/" target="_blank" rel="noopener noreferrer"><code>delete_blog</code></a> hook:</p>
<pre>function delete_blog_tables( $blog_id, $drop ) {
    if ( $drop ) {
        switch_to_blog( $blog_id );
        $wpdb-&gt;query( "DROP TABLE IF EXISTS {$wpdb-&gt;prefix}mytable" );
        restore_current_blog();
    }
}
add_action( 'delete_blog', 'delete_blog_tables' );</pre>
<p>If on the other hand you have one table for all blogs, and your rows have a <code>blog_id</code> column, do something more in the tune of:</p>
<pre>function delete_blog_tables( $blog_id, $drop ) {
    if ( $drop ) {
        $wpdb-&gt;query( "DELETE FROM {$wpdb-&gt;prefix}mytable WHERE blog_id = $blog_id" );
    }
}
add_action( 'delete_blog', 'delete_blog_tables' );</pre>
<p>There! All clean now!</p>
<h3>Cron</h3>
<p>You will want to use that handy little <em>foreach</em> loop in more than just the uninstall script. For instance, suppose you&#8217;re using <a href="https://developer.wordpress.org/plugins/cron/" target="_blank" rel="noopener noreferrer">cron</a>. If your plugin is network activated, cron will run once for the network, not once per blog. Assuming your cron handler needs to do stuff to each blog, you&#8217;d do something like:</p>
<pre>function cron_handler( ) {
    if ( is_plugin_active_for_network( 'myplugin/myplugin.php' ) ) {
        global $wpdb;
        foreach ( $wpdb-&gt;get_col( "SELECT blog_id FROM $wpdb-&gt;blogs" ) as $blog_id ) {
            switch_to_blog( $blog_id );
            do_stuff();
            restore_current_blog();
        }
    } else {
        do_stuff();
    }
}
add_action( 'cron_hook', 'cron_handler' );

if ( false === wp_next_scheduled( 'cron_hook' ) ) {
    wp_schedule_event( time(), 'every_now_and_then', 'cron_hook' );
}

function do_stuff() {
    // do your stuff here
}</pre>
<p>This way if your plugin runs network-wide, then your stuff is done on each one of the blogs on your multisite. But if the plugin is activated on the blog level, it does stuff only on the current blog.</p>
<p>Good stuff!</p>
<h2><del datetime="2017-08-13T18:22:13+00:00">Super secret plugin header: Network</del></h2>
<p><strong>Edit: This plugin header seems to not be respected any more; perhaps this is why it is not documented. It is part of WordPress history.</strong></p>
<p><del datetime="2017-08-13T18:22:13+00:00">Although you wouldn&#8217;t have guessed it from <a href="https://developer.wordpress.org/plugins/the-basics/header-requirements/" target="_blank" rel="noopener noreferrer">reading the relevant documentation</a>, the plugin header, i.e. that special block of comments that you put at the top of your main plugin file, can have something to say about network activation. (A tiny hint is given <a href="https://codex.wordpress.org/File_Header" target="_blank" rel="noopener noreferrer">in the Codex</a> that such a header exists, although the documentation does not currently bother explaining what it does. After all, if everything was documented, where would the fun be?)</del></p>
<p><del datetime="2017-08-13T18:22:13+00:00">If your plugin is intended to be <strong>only network activated</strong>, and not activated on the blog-level in multi-sites, then add this line to your plugin&#8217;s header:</del></p>
<pre>Network: True</pre>
<p><del datetime="2017-08-13T18:22:13+00:00">Now WordPress will not give the activation option to mere blog admins.</del></p>
<h2>Are you still with me?</h2>
<p>Awesome! There you have it! Now you know all that I currently know about WordPress multisite and network activated plugins. Which is arguably not a lot. If you know more and feel like sharing, you&#8217;re free to comment below.</p>
<p>Hopefully you now have some grasp of the types of things you should be looking out for when you take your plugin for a ride into the wacky world of network activation.</p>
<p>Take care.</p>
<p>Alex</p>
<p>The post <a href="https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/">🖧 The wacky world of network activated WordPress plugins in multisite</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/network-activated-wordpress-plugins/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>🐗 Building WordPress plugins with an increasingly verbose Gruntfile</title>
		<link>https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/</link>
					<comments>https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Fri, 24 Feb 2017 11:01:27 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[alias]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[grunt]]></category>
		<category><![CDATA[project management]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[wp-cli]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=192</guid>

					<description><![CDATA[<p>I am always aiming for a hierarchical Gruntfile.js structure that reminds me of increasingly verbose memes. And you should, too.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/">🐗 Building WordPress plugins with an increasingly verbose Gruntfile</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>When building WordPress plugins, I&#8217;m using <strong>gruntjs</strong> for all my project tooling needs. I am always aiming for a hierarchical Gruntfile.js structure that reminds me of <a href="http://knowyourmeme.com/memes/increasingly-verbose-memes" target="_blank" rel="noopener noreferrer">increasingly verbose memes</a>. And you should, too.</p>
<p>No matter which tool you&#8217;re using, building, testing, packaging and deploying a WordPress plugin should be one command. That way when you&#8217;re dev testing, you are actually testing the whole process, including installation, activation and deactivation of the plugin&#8217;s zip file. <a href="https://github.com/sindresorhus/grunt-shell" target="_blank" rel="noopener noreferrer">grunt-shell</a> and <a href="http://wp-cli.org/" target="_blank" rel="noopener noreferrer">wp-cli</a> are your friends. Use <a href="http://wp-cli.org/commands/option/" target="_blank" rel="noopener noreferrer"><strong>wp option</strong></a> liberally in your dev deployment code.</p>
<p>My Gruntfile.js describes all the steps needed between my code sitting in my working directory and the same code running in my WordPress dev environment.</p>
<p><a href="http://gruntjs.com/creating-tasks" target="_blank" rel="noopener noreferrer">Grunt alias tasks</a> let you define this process hierarchically. Use them! I aim to model the process so that at some level it has some resemblance to steps from the waterfall model: <strong>build</strong>, <strong>package</strong>, <strong>test</strong>, and <strong>deploy</strong> are verbs that I always aim to define in my script. Then, at a lower level I can define which grunt plugins actually accomplish each step.</p>
<p>When you have defined the steps in this way, then you can begin to create builds of varying quality or for various purposes. Testing takes too long to run every time in your development cycle? Create a quick build that excludes it. Want to have separate builds with and without documentation included? Define another package verb. Want to create a deliverable package but not deploy it? Create a task that omits the deploy verb.</p>
<p>The following is from an actual project I&#8217;m working on. Don&#8217;t worry about the details. You have your own workflow. All I&#8217;m saying is that you will benefit from structuring your script hierarchically.</p>
<p>Here&#8217;s my increasingly verbose meme. Make it viral!</p>
<p><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-193" src="http://www.alexgeorgiou.gr/wp-content/uploads/2017/02/grunt-meme.jpeg" alt="Increasingly verbose Gruntfile" width="640" height="800" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2017/02/grunt-meme.jpeg 640w, https://www.alexgeorgiou.gr/wp-content/uploads/2017/02/grunt-meme-240x300.jpeg 240w" sizes="auto, (max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /></p>
<p>The post <a href="https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/">🐗 Building WordPress plugins with an increasingly verbose Gruntfile</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/increasingly-verbose-gruntfile/feed/</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
			</item>
		<item>
		<title>🗨 Dismissible notices that persist when refreshing the WordPress admin screens</title>
		<link>https://www.alexgeorgiou.gr/persistently-dismissible-notices-wordpress/</link>
					<comments>https://www.alexgeorgiou.gr/persistently-dismissible-notices-wordpress/#comments</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 28 Nov 2016 12:15:47 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[admin screens]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[dismiss]]></category>
		<category><![CDATA[dismissable]]></category>
		<category><![CDATA[dismissible]]></category>
		<category><![CDATA[error]]></category>
		<category><![CDATA[error handling]]></category>
		<category><![CDATA[error reporting]]></category>
		<category><![CDATA[notification]]></category>
		<category><![CDATA[popup]]></category>
		<category><![CDATA[wordpress]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=154</guid>

					<description><![CDATA[<p>In this article we will go through some code that I like to use to make dismissible  notices where the dismissal persists between page refreshes in the WordPress administration screens.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/persistently-dismissible-notices-wordpress/">🗨 Dismissible notices that persist when refreshing the WordPress admin screens</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p><em>In this article we will go through some code that I like to use to make dismissible  notices where the dismissal persists between page refreshes in the WordPress administration screens.</em></p>
<p>&nbsp;</p>
<h2><img loading="lazy" decoding="async" class="aligncenter wp-image-163 size-full" title="Dismissible notices that persist when refreshing the WordPress admin screens" src="http://www.alexgeorgiou.gr/wp-content/uploads/2016/11/wp-notices-1.png" alt="Make dismissible admin notices where the dismissal persists between page refreshes in the WordPress administration screens." width="963" height="480" srcset="https://www.alexgeorgiou.gr/wp-content/uploads/2016/11/wp-notices-1.png 963w, https://www.alexgeorgiou.gr/wp-content/uploads/2016/11/wp-notices-1-300x150.png 300w, https://www.alexgeorgiou.gr/wp-content/uploads/2016/11/wp-notices-1-768x383.png 768w, https://www.alexgeorgiou.gr/wp-content/uploads/2016/11/wp-notices-1-800x400.png 800w" sizes="auto, (max-width: 599px) calc(100vw - 50px), (max-width: 767px) calc(100vw - 70px), (max-width: 991px) 429px, (max-width: 1199px) 637px, 354px" /></h2>
<h2>Some things to note about dismissible notices</h2>
<p>Don&#8217;t you just hate how this functionality has <strong>still</strong> not made it into core? <a href="https://make.wordpress.org/core/2015/04/23/spinners-and-dismissible-admin-notices-in-4-2/" target="_blank" rel="noopener noreferrer">Legend has it</a> that dismissible notices were introduced back in WordPress 4.2, but that only means that when you click the little &#8220;x&#8221; at the top right, the notice box becomes hidden. The way to enable this is that you add a <code>.is-dismissible</code> class into your notice&#8217;s markup:</p>
<pre>&lt;div class="notice notice-warn is-dismissible"&gt;your message here&lt;/div&gt;</pre>
<p>And that hides the box. Until the next page refresh. This is not only annoying, it also goes <a href="https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/" target="_blank" rel="noopener noreferrer">against the WordPress.org guidelines</a>:</p>
<blockquote><p>Upgrade prompts, notices, and alerts should be limited in scope and used sparingly or only on the plugin’s setting page. Any site wide notices or embedded dashboard widgets <em>must</em> be dismissible. Error messages and alerts should include information on how to resolve the situation, and remove themselves when completed.</p></blockquote>
<p>So for those of us aspiring to be hosted on <strong>WordPress.org</strong> it&#8217;s a necessity, rather than a luxury feature, to be able to let the user dismiss notices persistently.</p>
<h2>Requirements analysis</h2>
<p>WordPress tradition has it that you <strong>store an option in the database</strong>. That way you know not to show the same notice again. But this is something that you have to do over and over again, so it&#8217;s exactly the kind of thing that you want to <strong>include in all your code as a library</strong>. You want something that can be <strong>easily called from wherever</strong>, so the tried and true singleton pattern will do. You need the ability to be able to <strong>assign a slug to your notice</strong>, so you know how to store it in the options table. But you also want to be able to <strong>show the occasional non-dismissible notice</strong>. Finally, let&#8217;s be nice and <strong>clean up after ourselves</strong>. The options will be deleted when our plugin is uninstalled.</p>
<h2>Let&#8217;s do this!</h2>
<h3>Writing code with class</h3>
<p>We need something that can be included from our plugin (or theme). Let&#8217;s make a singleton class that can hold arrays of different types of notices (success, info, warn, error):</p>
<pre>&lt;?php

// don't load directly
defined( 'ABSPATH' ) || die( '-1' );

if ( ! class_exists( 'MyPlugin_Admin_Notices' ) ) {

    class MyPlugin_Admin_Notices {

        private static $_instance;
        private $admin_notices;
        const TYPES = 'error,warning,info,success';

        private function __construct() {
            $this-&gt;admin_notices = new stdClass();
            foreach ( explode( ',', self::TYPES ) as $type ) {
                $this-&gt;admin_notices-&gt;{$type} = array();
            }
        }

        public static function get_instance() {
            if ( ! ( self::$_instance instanceof self ) ) {
                self::$_instance = new self();
            }
            return self::$_instance;
        }
    }
}

MyPlugin_Admin_Notices::get_instance();</pre>
<p>Nice. This is a useful bucket where we can throw in notices. They will be retrieved for rendering only later, when we will add code to the <code>admin_notices</code> action.</p>
<h3>An API for entering notices from our code</h3>
<p>For now, let&#8217;s add in some methods to our class for populating our notice arrays:</p>
<pre>public function error( $message, $dismiss_option = false ) {
    $this-&gt;notice( 'error', $message, $dismiss_option );
}

public function warning( $message, $dismiss_option = false ) {
    $this-&gt;notice( 'warning', $message, $dismiss_option );
}

public function success( $message, $dismiss_option = false ) {
    $this-&gt;notice( 'success', $message, $dismiss_option );
}

public function info( $message, $dismiss_option = false ) {
    $this-&gt;notice( 'info', $message, $dismiss_option );
}

private function notice( $type, $message, $dismiss_option ) {
    $notice = new stdClass();
    $notice-&gt;message = $message;
    $notice-&gt;dismiss_option = $dismiss_option;

    $this-&gt;admin_notices-&gt;{$type}[] = $notice;
}</pre>
<p><strong>Notice the visibility of the functions.</strong> The four public functions that comprise our API all defer to the private function that does the data collection. We&#8217;ll let the user enter a message string, and optionally give a slug with the <code>dismiss_option</code> parameter. If set, this will be part of the database option&#8217;s slug.</p>
<h3>Writing the markup</h3>
<p>We have a mechanism for adding notices into our memory, now let&#8217;s dump them to the admin area as markup. This will happen on the <code>admin_notices</code> action, so first add this to the constructor:</p>
<pre>add_action( 'admin_notices', array( &amp;$this, 'action_admin_notices' ) );</pre>
<p>and then let&#8217;s actually write the markup. I&#8217;ve chosen to show notices in decreasing levels of severity, hence the nested loops:</p>
<pre>public function action_admin_notices() {
    foreach ( explode( ',', self::TYPES ) as $type ) {
        foreach ( $this-&gt;admin_notices-&gt;{$type} as $admin_notice ) {

            $dismiss_url = add_query_arg( array(
                'myplugin_dismiss' =&gt; $admin_notice-&gt;dismiss_option
            ), admin_url() );

            if ( ! get_option( "myplugin_dismissed_{$admin_notice-&gt;dismiss_option}" ) ) {
                ?&gt;&lt;div
                    class="notice myplugin-notice notice-&lt;?php echo $type;

                    if ( $admin_notice-&gt;dismiss_option ) {
                        echo ' is-dismissible" data-dismiss-url="' . esc_url( $dismiss_url );
                    } ?&gt;"&gt;

                    &lt;h2&gt;&lt;?php echo "My Plugin $type"; ?&gt;&lt;/h2&gt;
                    &lt;p&gt;&lt;?php echo $admin_notice-&gt;message; ?&gt;&lt;/p&gt;

                &lt;/div&gt;&lt;?php
            }
        }
    }
}</pre>
<p><strong>Note how we show a notice only if the DB does not have a corresponding option</strong> named <code>myplugin_dismissed_$dismiss_option</code>. We&#8217;ll need a mechanism to set that DB option when the user clicks to dismiss the notice. We will do this from JavaScript, making a call with that option name as a GET parameter, so that we know to set the correct database option.</p>
<h3>Notifying the backend from the admin front</h3>
<p>When a notice is dismissible, we output a <code>data-dismiss-url</code> attribute in the HTML. We&#8217;ll use that from JavaScript to make a call to that URL:</p>
<pre>/**
 * Admin code for dismissing notifications.
 *
 */
(function( $ ) {
    'use strict';
    $( function() {
        $( '.myplugin-notice' ).on( 'click', '.notice-dismiss', function( event, el ) {
            var $notice = $(this).parent('.notice.is-dismissible');
            var dismiss_url = $notice.attr('data-dismiss-url');
            if ( dismiss_url ) {
                $.get( dismiss_url );
            }
        });
    } );
})( jQuery );</pre>
<p>Pretty standard stuff. When a dismissible notice from our plugin is clicked on its <code>.notice-dismiss</code>, get the <code>dismiss_url</code> and call it. I guess I could have used a fancy framework like <a href="http://vanilla-js.com/" target="_blank" rel="noopener noreferrer">VanillaJS</a> for this, but I chose plain old jQuery instead :-p</p>
<p>Don&#8217;t forget the usual enqueue script shenanigans:</p>
<pre>public function action_admin_enqueue_scripts() {
    wp_enqueue_script( 'jquery' );
    wp_enqueue_script(
        'myplugin-notify',
        plugins_url( 'assets/scripts/myplugin-notify.js', __FILE__ ),
        array( 'jquery' )
    );
}</pre>
<p>and add this to our constructor so the enqueuing will actually happen:</p>
<pre>add_action( 'admin_enqueue_scripts', array( &amp;$this, 'action_admin_enqueue_scripts' ) );</pre>
<p>Perfect. Our little piece of JavaScript code now lives in the WordPress admin screens.</p>
<h3>Setting the DB option</h3>
<p>Now we&#8217;ll just need to catch the request on the PHP side and set a database option. Let&#8217;s do this on the <code>admin_init</code> action:</p>
<pre>add_action( 'admin_init', array( &amp;$this, 'action_admin_init' ) );</pre>
<p>&#8230;and this is the code that will set an option in the database. Note how the name is constructed from the GET parameter in the request URL.</p>
<pre>public function action_admin_init() {
    $dismiss_option = filter_input( INPUT_GET, 'myplugin_dismiss', FILTER_SANITIZE_STRING );
    if ( is_string( $dismiss_option ) ) {
        update_option( "myplugin_dismissed_$dismiss_option", true );
        wp_die();
    }
}</pre>
<p>Once the option is set, we can just let WordPress die. No need to return the admin interface to the browser, this is just a background AJAX call.</p>
<h3>Cleaning up after ourselves</h3>
<p>Unfortunately, even in this day and age, many developers don&#8217;t think it&#8217;s important to clean up the trash they leave in the poor user&#8217;s database. This is even more infuriating when you consider that it rarely takes more than a couple of lines of code. Don&#8217;t be that guy. Create an <code>uninstall.php</code> file with the following content.</p>
<pre>&lt;?php
if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    global $wpdb;
    $wpdb-&gt;query( 'DELETE FROM wp_options WHERE option_name LIKE "myplugin_dismissed_%";' );
}</pre>
<p>We can exploit the fact that we know the common prefix of all the dismissal DB options, and thus we can delete them all in one go, circumventing the <code><a href="https://codex.wordpress.org/Function_Reference/delete_option" target="_blank" rel="noopener noreferrer">delete_option</a>()</code> function altogether:</p>
<h3>Expecting the unexpected</h3>
<p><strong>If you put together all of the above you already have a nice way to show persistent dismissible admin notices.</strong> But why stop there? We can hack away some more and make sure that any runtime errors in our code show up as notices too.</p>
<p>Imagine a clueless user sees your theme not working. Which do you prefer? To have to explain to them how to enable debugging and how to find and send you the logs from the wordpress unix directory on the host? Or do you simply ask them to copy the error on the screen and send it to you via email?</p>
<p>Observe this little hack that takes advantage of the <a href="http://php.net/manual/en/function.set-error-handler.php" target="_blank" rel="noopener noreferrer">PHP error reporting mechanism</a>:</p>
<pre>public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext ) {
    if ( ! ( error_reporting() &amp; $errno ) ) {
        // This error code is not included in error_reporting
        return;
    }

    $message = "errstr: $errstr, errfile: $errfile, errline: $errline, PHP: " . PHP_VERSION . " OS: " . PHP_OS;

    $self = self::get_instance();
    switch ($errno) {
        case E_USER_ERROR:
            $self-&gt;error( $message );
            break;

        case E_USER_WARNING:
            $self-&gt;warning( $message );
            break;

        case E_USER_NOTICE:
        default:
            $self-&gt;notice( $message );
            break;
    }

    // write to wp-content/debug.log if logging enabled
    error_log( $message );

    // Don't execute PHP internal error handler
    return true;
}</pre>
<p>Notice how this method is static, since we&#8217;d like to be able to call it from both dynamic and static contexts. We can now hook it up to PHP&#8217;s error reporting like so:</p>
<pre><a href="http://php.net/manual/en/function.set-error-handler.php" target="_blank" rel="noopener noreferrer">set_error_handler</a>( array( 'MyPlugin_Admin_Notices', 'error_handler' ) );</pre>
<p>and after your code ends, unhook it so as not to interfere with the WordPress core or other components:</p>
<pre><a href="http://php.net/manual/en/function.restore-error-handler.php" target="_blank" rel="noopener noreferrer">restore_error_handler</a>();</pre>
<p>This will pop your error handler from a stack and return to whatever error handling mechanism was there before. <strong>Make a habit of surrounding your function bodies with these two lines</strong>, and you will know that whatever happens, you&#8217;ll at least get a visible user-friendly <strong>and</strong> developer-friendly error message on the admin screens.</p>
<h2>Putting it all together</h2>
<h3>JS</h3>
<pre>/**
 * Admin code for dismissing notifications.
 *
 */
(function( $ ) {
    'use strict';
    $( function() {
        $( '.myplugin-notice' ).on( 'click', '.notice-dismiss', function( event, el ) {

            var $notice = $(this).parent('.notice.is-dismissible');
            var dismiss_url = $notice.attr('data-dismiss-url');
            if ( dismiss_url ) {
                $.get( dismiss_url );
            }
        });
    } );
})( jQuery );</pre>
<h3>PHP</h3>
<pre>&lt;?php
if ( defined( 'WP_UNINSTALL_PLUGIN' ) ) {
    global $wpdb;
    $wpdb-&gt;query( 'DELETE FROM wp_options WHERE option_name LIKE "/* @echo slugus */_dismissed_%";' );
}</pre>
<pre>&lt;?php
// don't load directly
defined( 'ABSPATH' ) || die( '-1' );

if ( ! class_exists( 'MyPlugin_Admin_Notices' ) ) {

    class MyPlugin_Admin_Notices {

        private static $_instance;
        private $admin_notices;
        const TYPES = 'error,warning,info,success';

        private function __construct() {
            $this-&gt;admin_notices = new stdClass();
            foreach ( explode( ',', self::TYPES ) as $type ) {
                $this-&gt;admin_notices-&gt;{$type} = array();
            }
            add_action( 'admin_init', array( &amp;$this, 'action_admin_init' ) );
            add_action( 'admin_notices', array( &amp;$this, 'action_admin_notices' ) );
            add_action( 'admin_enqueue_scripts', array( &amp;$this, 'action_admin_enqueue_scripts' ) );
        }

        public static function get_instance() {
            if ( ! ( self::$_instance instanceof self ) ) {
                self::$_instance = new self();
            }
            return self::$_instance;
        }

        public function action_admin_init() {
            $dismiss_option = filter_input( INPUT_GET, 'myplugin_dismiss', FILTER_SANITIZE_STRING );
            if ( is_string( $dismiss_option ) ) {
                update_option( "myplugin_dismissed_$dismiss_option", true );
                wp_die();
            }
        }

        public function action_admin_enqueue_scripts() {
            wp_enqueue_script( 'jquery' );
            wp_enqueue_script(
                'myplugin-notify',
                plugins_url( 'assets/scripts/myplugin-notify.js', __FILE__ ),
                array( 'jquery' )
            );
        }

        public function action_admin_notices() {
            foreach ( explode( ',', self::TYPES ) as $type ) {
                foreach ( $this-&gt;admin_notices-&gt;{$type} as $admin_notice ) {

                    $dismiss_url = add_query_arg( array(
                        'myplugin_dismiss' =&gt; $admin_notice-&gt;dismiss_option
                    ), admin_url() );

                    if ( ! get_option( "myplugin_dismissed_{$admin_notice-&gt;dismiss_option}" ) ) {
                        ?&gt;&lt;div
                            class="notice myplugin-notice notice-&lt;?php echo $type;

                            if ( $admin_notice-&gt;dismiss_option ) {
                                echo ' is-dismissible" data-dismiss-url="' . esc_url( $dismiss_url );
                            } ?&gt;"&gt;

                            &lt;h2&gt;&lt;?php echo "My Plugin $type"; ?&gt;&lt;/h2&gt;
                            &lt;p&gt;&lt;?php echo $admin_notice-&gt;message; ?&gt;&lt;/p&gt;

                        &lt;/div&gt;&lt;?php
                    }
                }
            }
        }

        public function error( $message, $dismiss_option = false ) {
            $this-&gt;notice( 'error', $message, $dismiss_option );
        }

        public function warning( $message, $dismiss_option = false ) {
            $this-&gt;notice( 'warning', $message, $dismiss_option );
        }

        public function success( $message, $dismiss_option = false ) {
            $this-&gt;notice( 'success', $message, $dismiss_option );
        }

        public function info( $message, $dismiss_option = false ) {
            $this-&gt;notice( 'info', $message, $dismiss_option );
        }

        private function notice( $type, $message, $dismiss_option ) {
            $notice = new stdClass();
            $notice-&gt;message = $message;
            $notice-&gt;dismiss_option = $dismiss_option;

            $this-&gt;admin_notices-&gt;{$type}[] = $notice;
        }

	public static function error_handler( $errno, $errstr, $errfile, $errline, $errcontext ) {
		if ( ! ( error_reporting() &amp; $errno ) ) {
			// This error code is not included in error_reporting
			return;
		}

		$message = "errstr: $errstr, errfile: $errfile, errline: $errline, PHP: " . PHP_VERSION . " OS: " . PHP_OS;

		$self = self::get_instance();

		switch ($errno) {
			case E_USER_ERROR:
				$self-&gt;error( $message );
				break;

			case E_USER_WARNING:
				$self-&gt;warning( $message );
				break;

			case E_USER_NOTICE:
			default:
				$self-&gt;notice( $message );
				break;
		}

		// write to wp-content/debug.log if logging enabled
		error_log( $message );

		// Don't execute PHP internal error handler
		return true;
	}
    }
}

MyPlugin_Admin_Notices::get_instance();</pre>
<h2>Thanks for reading</h2>
<p>Please comment on what you liked or didn&#8217;t like in my code. What would you have done differently? Can you spot any bugs?</p>
<p>Until next time. Dismissed!</p>
<p>The post <a href="https://www.alexgeorgiou.gr/persistently-dismissible-notices-wordpress/">🗨 Dismissible notices that persist when refreshing the WordPress admin screens</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/persistently-dismissible-notices-wordpress/feed/</wfw:commentRss>
			<slash:comments>8</slash:comments>
		
		
			</item>
		<item>
		<title>☑ WordPress checklists to go through before publishing a theme or plugin</title>
		<link>https://www.alexgeorgiou.gr/wordpress-checklists-publish-theme-plugin/</link>
					<comments>https://www.alexgeorgiou.gr/wordpress-checklists-publish-theme-plugin/#respond</comments>
		
		<dc:creator><![CDATA[alexg]]></dc:creator>
		<pubDate>Mon, 07 Nov 2016 11:48:55 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[checklist]]></category>
		<category><![CDATA[codecanyon]]></category>
		<category><![CDATA[coding standards]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[envato]]></category>
		<category><![CDATA[publish]]></category>
		<category><![CDATA[quality assurance]]></category>
		<category><![CDATA[release]]></category>
		<category><![CDATA[testing]]></category>
		<category><![CDATA[themeforest]]></category>
		<category><![CDATA[wordpress]]></category>
		<category><![CDATA[wordpress.org]]></category>
		<guid isPermaLink="false">http://www.alexgeorgiou.gr/?p=142</guid>

					<description><![CDATA[<p>In this meta-article I will list eight WordPress checklists and a book that you will want to go through, before you publish a theme or plugin.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-checklists-publish-theme-plugin/">☑ WordPress checklists to go through before publishing a theme or plugin</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></description>
										<content:encoded><![CDATA[<p>In this meta-article I will list <em>eight WordPress checklists</em> and a book that you will want to go through, before you publish a theme or plugin. So before committing to the <em>wordpress.org</em> SVN or uploading to <em>Envato</em>, bookmark this page and start double-checking all the resources listed here!</p>
<h2><a title="Read 'Ten Things Every WordPress Plugin Developer Should Know'" href="https://www.smashingmagazine.com/2011/03/ten-things-every-wordpress-plugin-developer-should-know/" target="_blank" rel="bookmark noopener noreferrer">Ten Things Every WordPress Plugin Developer Should Know</a></h2>
<p>Actually you&#8217;ll probably want to read this before even starting your project; but better late than never! It&#8217;s packed with essential info that you should know. If you&#8217;ve missed any of the info in this article, go back to your code and fix it. There&#8217;s stuff about common practices, coding standards, etc.</p>
<h2 class="handbook-page-title"><a href="https://make.wordpress.org/core/handbook/best-practices/coding-standards/php/" target="_blank" rel="noopener noreferrer">PHP Coding Standards</a></h2>
<p>This is a list of PHP coding standards that WordPress developers follow, both in core and in themes and plugins. It&#8217;s a requirement for submission to <em>wordpress.org</em> but it would be really nice to follow as many of these as possible even if you&#8217;re not planning to submit your code there.</p>
<p>Ideally you&#8217;ll want to use some tools for this. A quick search on <em>GitHub</em> reveals a number of syntax checkers. Here&#8217;s one that I use with <em>Eclipse</em>: <a href="https://github.com/edpittol/wordpress-eclipse-formatter" target="_blank" rel="noopener noreferrer">https://github.com/edpittol/wordpress-eclipse-formatter</a>, and another one for <em>CodeSniffer</em> (which can integrate nicely with a number of IDEs): <a href="https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards" target="_blank" rel="noopener noreferrer">https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards</a>.</p>
<p>Whatever you use, it doesn&#8217;t hurt to double check your code one last time. In <em>Eclipse Formatter</em> for instance, there is no way to check for <a href="https://en.wikipedia.org/wiki/Yoda_conditions" target="_blank" rel="noopener noreferrer">&#8220;Yoda&#8221; conditions</a> as far as I know, so you&#8217;ll want to manually check for those if you&#8217;re using it.</p>
<h2 class="handbook-page-title"><a href="https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/">JavaScript Coding Standards</a></h2>
<p>Have you checked your PHP code for compliance to the coding standards? Good! Get ready to do it again, this time for your JavaScript files; at least the unminified copies of them. Here&#8217;s a project that uses <em>ESLint</em> to help you enforce the standards: <a href="https://www.npmjs.com/package/eslint-plugin-wordpress" target="_blank" rel="noopener noreferrer">https://www.npmjs.com/package/eslint-plugin-wordpress</a>. If you have a lot of JavaScript in your code, it might pay to integrate this into your pre-build checks, but as with PHP, always have one last look using your eyeballs.</p>
<h2><a href="https://wordpress.org/plugins/about/" target="_blank" rel="noopener noreferrer">WordPress plugins</a></h2>
<p>If you&#8217;re writing a plugin, this is a short list of conditions that your project must meet before you publish to <em>WordPress.org</em>. It has more to do with functionality than standards, and essentially it&#8217;s about not being evil with your code.</p>
<h2><a href="https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/" target="_blank" rel="noopener noreferrer">Detailed Plugin Guidelines</a></h2>
<p>This is essentially a longer version of the above. A definite must read if you&#8217;re publishing to <em>WordPress.org</em>. It&#8217;s about licensing, versioning, what you can and can&#8217;t do, etc.</p>
<h2 id="theme-review-process" class="toc-heading" tabindex="-1"><a href="https://make.wordpress.org/themes/handbook/review/#theme-review-process" target="_blank" rel="noopener noreferrer">Theme Review Process</a></h2>
<p>If you&#8217;re writing a theme that you plan to publish to <em>WordPress.org</em>, check that it at least conforms to all the <a href="https://make.wordpress.org/themes/handbook/review/required/" target="_blank" rel="noopener noreferrer">required items</a> and try to conform to as many <a href="https://make.wordpress.org/themes/handbook/review/recommended/" target="_blank" rel="noopener noreferrer">recommended items</a> as possible. The write-up for the recommended guidelines is not complete as of the writing of this article, but there&#8217;s some very useful guidelines in there.</p>
<h2><a href="https://codex.wordpress.org/Theme_Unit_Test" target="_blank" rel="noopener noreferrer">Theme Unit Test</a></h2>
<p>Again, if you&#8217;re writing a theme for <em>WordPress.org</em>, you will also want to check a number of things against the provided <a href="https://codex.wordpress.org/Theme_Unit_Test" target="_blank" rel="noopener noreferrer">Theme Unit Test</a>. It&#8217;s probably a good idea to do this even if you&#8217;re publishing elsewhere.</p>
<h2><a href="https://help.market.envato.com/hc/en-us/articles/202500774-ThemeForest-General-File-Preparation-Guidelines" target="_blank" rel="noopener noreferrer">ThemeForest General File Preparation Guidelines</a></h2>
<p>If you&#8217;re publishing to <em>ThemeForest</em>, you will have to follow these guidelines on how to bundle your files.</p>
<h2><a href="https://help.market.envato.com/hc/en-us/articles/202501494-WordPress-Phase-1-Submission-Requirements-Q-A" target="_blank" rel="noopener noreferrer">WordPress Phase 1 Submission Requirements &#8211; Q&amp;A</a></h2>
<p>This is a list of guidelines to follow before submitting to <em>ThemeForest</em>. There&#8217;s a number of things listed that are not allowed, so make sure to check.</p>
<h2><a href="https://help.market.envato.com/hc/en-us/articles/203269650-A-step-by-step-guide-to-the-upload-process" target="_blank" rel="noopener noreferrer">A step-by-step guide to the upload process</a></h2>
<p>When uploading to <em>ThemeForest</em>, you will also want to follow these guidelines. They are mostly about the upload process but also include some information about how to bundle your files.</p>
<h2>Professional WordPress: Design and Development</h2>
<p><a style="width: 120px; height: 240px; float: right; margin-left: 1em;" href="https://amzn.to/3W4EePm" width="300" height="150"><br />
<img decoding="async" src="https://m.media-amazon.com/images/I/71OD8C2NZNL._SY466_.jpg" /><br />
</a><br />
Finally, this is a book, not an actual checklist, but still I recommend that you at least glance through the table of contents. I&#8217;ve found this book very helpful. It covers all topics related to WordPress development and it&#8217;s very likely that whatever it is you&#8217;re trying to do, there might be a better way of doing it. WordPress is a complex system and it&#8217;s only getting more complex by the day.</p>
<p>My recommendation is that you go through the table of contents and read any chapter that talks about things you do in your theme or plugin. There are things you might have missed.</p>
<div style="clear: right;"></div>
<hr />
<p>Too meta? Perhaps! But no matter what and where you&#8217;re publishing, if you want to be professional about it, don&#8217;t assume you know everything. At least glance through all of these guidelines.</p>
<p>The post <a href="https://www.alexgeorgiou.gr/wordpress-checklists-publish-theme-plugin/">☑ WordPress checklists to go through before publishing a theme or plugin</a> appeared first on <a href="https://www.alexgeorgiou.gr">Alexandros Georgiou</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.alexgeorgiou.gr/wordpress-checklists-publish-theme-plugin/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
