Completed
Push — master ( 4487f0...44a470 )
by Askupa
01:52
created

Manager.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Amarkal\Metabox;
4
5
/**
6
 * Metabox Manager adds metaboxes to WordPress posts
7
 */
8
class Manager
9
{
10
    /**
11
     * @var Singleton The reference to *Singleton* instance of this class
12
     */
13
    private static $instance;
14
    
15
    /**
16
     * @var Array Stores all the registered metaboxes
17
     */
18
    private $metaboxes = array();
19
    
20
    /**
21
     * Security nonce action
22
     */
23
    const NONCE_ACTION = 'amarkal_metabox';
24
    
25
    /**
26
     * Returns the *Singleton* instance of this class.
27
     *
28
     * @return Singleton The *Singleton* instance.
29
     */
30
    public static function get_instance()
31
    {
32
        if( null === static::$instance ) 
33
        {
34
            static::$instance = new static();
35
        }
36
        return static::$instance;
37
    }
38
    
39
    /**
40
     * Private constructor to prevent instantiation
41
     */
42
    private function __construct() 
43
    {
44
        $this->init();
45
    }
46
    
47
    /**
48
     * Add a metabox.
49
     * 
50
     * @param string $id
51
     * @param array $args
52
     * @throws \RuntimeException if the given metabox id has already been registered
53
     */
54
    public function add( $id, array $args )
55
    {
56
        if( !in_array($id, $this->metaboxes) )
57
        {
58
            $this->metaboxes[$id] = array_merge($this->default_args(), $args);
59
            $this->metaboxes[$id]['form'] = new \Amarkal\UI\Form($args['fields']);
60
        }
61
        else throw new \RuntimeException("A metabox with id '$id' has already been registered.");
62
    }
63
    
64
    /**
65
     * Render a metabox.
66
     * 
67
     * @param WP_Post $post
68
     * @param array $args
69
     */
70
    public function render( $post, $args )
71
    {
72
        $metabox = $this->metaboxes[$args['id']];
73
        
74
        // Print the errors from the previous processing
75
        $this->print_errors($post->ID, $metabox);
76
        
77
        // Update component values before rendering
78
        $this->update_form($metabox, $post->ID);
79
        
80
        // Render the metabox with a nonce
81
        wp_nonce_field(self::NONCE_ACTION, $args['id'].'_nonce');
82
        
83
        $template = new \Amarkal\UI\Template(
84
            array('components' => $metabox['form']->get_components()),
85
            __DIR__.'/Form.phtml'
86
        );
87
        
88
        $template->render(true);
89
    }
90
    
91
    /**
92
     * Internally used to register metaboxes.
93
     */
94
    public function add_meta_boxes()
95
    {
96
        foreach( $this->metaboxes as $id => $args )
97
        {
98
            \add_meta_box(
99
                $id,
100
                $args['title'],
101
                array($this, 'render'),
102
                $args['screen'],
103
                $args['context'],
104
                $args['priority']
105
            );
106
        }
107
    }
108
    
109
    /**
110
     * Save metaboxes data for a given page.
111
     * 
112
     * @param number $post_id
113
     */
114
    public function save_meta_boxes( $post_id )
115
    {
116
        /**
117
         * A note on security:
118
         * 
119
         * We need to verify this came from the our screen and with proper authorization,
120
         * because save_post can be triggered at other times. since metaboxes can 
121
         * be removed - by having a nonce field in only one metabox there is no 
122
         * guarantee the nonce will be there. By placing a nonce field in each 
123
         * metabox you can check if data from that metabox has been sent 
124
         * (and is actually from where you think it is) prior to processing any data.
125
         * @see http://wordpress.stackexchange.com/a/49460/25959
126
         */
127
 
128
        /*
129
         * If this is an autosave, our form has not been submitted,
130
         * so we don't want to do anything.
131
         */
132
        if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) 
133
        {
134
            return $post_id;
135
        }
136
137
        // Check the user's permissions.
138
        $post_type = filter_input(INPUT_POST, 'post_type');
139
        if( null !== $post_type && !current_user_can('edit_'.$post_type, $post_id) )
140
        {
141
            return $post_id;
142
        }
143
144
        // Update the meta fields.
145
        foreach( $this->metaboxes as $id => $metabox )
146
        {
147
            $this->save_meta_box( $post_id, $id, $metabox );
148
        }
149
    }
150
    
151
    /**
152
     * Save the data of a single metabox.
153
     * 
154
     * @param number $post_id
155
     * @param string $id
156
     * @param array $metabox
157
     */
158
    public function save_meta_box( $post_id, $id, $metabox )
159
    {
160
        $nonce_name   = $id.'_nonce';
161
        $nonce_value  = filter_input(INPUT_POST, $nonce_name);
162
        $new_instance = filter_input_array(INPUT_POST);
163
        
164
        // Check if our nonce is set and verify it
165
        if( null === $nonce_value || !wp_verify_nonce($nonce_value, self::NONCE_ACTION) ) 
166
        {
167
            return $post_id;
168
        }
169
170
        $this->update_form($metabox, $post_id, $new_instance);
171
    }
172
    
173
    /**
174
     * Print custom metabox style.
175
     */
176
    public function print_style() 
177
    {
178
        $cs = get_current_screen();
179
        
180
        foreach( $this->metaboxes as $metabox )
181
        {
182
            if( $metabox['screen'] === $cs->id )
183
            {
184
                echo '<style>';
185
                include 'metabox.css';
186
                echo '</style>';
187
                return;
188
            }
189
        }
190
    }
191
    
192
    /**
193
     * Print all errors stored in a transient for a given post ID.
194
     * 
195
     * @param number $post_id
196
     * @param array $metabox
197
     */
198
    public function print_errors( $post_id, $metabox )
199
    {
200
        $errors  = \get_transient("amarkal_metabox_errors_$post_id");
201
        
202
        if( $errors )
203
        {
204
            foreach( $errors as $name => $error )
205
            {
206
                $component = $metabox['form']->get_component($name);
207
                echo "<div class=\"notice notice-error\"><p><strong>{$component->title}</strong> $error</p></div>";
208
            }
209
        }
210
        
211
        \delete_transient("amarkal_metabox_errors_$post_id");
212
    }
213
    
214
    /**
215
     * Initiate the metaboxes by adding action hooks for printing and saving.
216
     */
217
    private function init()
218
    {
219
        \add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
220
        \add_action( 'save_post', array( $this, 'save_meta_boxes' ) );
221
        \add_action( 'admin_footer', array( $this, 'print_style' ) );
222
    }
223
    
224
    /**
225
     * Update the form data for the given given metabox. If the $new_instance
226
     * contains new data, it will be saved into the db and if there are any 
227
     * validation errors they will be printed.
228
     * 
229
     * @param array $metabox
230
     * @param number $post_id
231
     * @param array $new_instance
232
     */
233
    private function update_form( $metabox, $post_id, array $new_instance = array() )
234
    {
235
        $old_instance   = $this->get_old_instance($metabox, $post_id);
236
        $final_instance = $metabox['form']->update( $new_instance, $old_instance );
237
238
        // Update db if there is new data to be saved
239
        if( array() !== $new_instance )
240
        {
241
            $this->update_post_meta($final_instance, $post_id);
0 ignored issues
show
$post_id is of type integer|double, but the function expects a object<Amarkal\Metabox\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
242
            
243
            /**
244
             * We need to store all errors in a transient since WordPress does
245
             * a redirect to post.php and then back to our post, which clears
246
             * the execution thread. See https://www.sitepoint.com/displaying-errors-from-the-save_post-hook-in-wordpress/
247
             */
248
            \set_transient( "amarkal_metabox_errors_$post_id", $metabox['form']->get_errors(), 60 );
249
        }
250
    }
251
    
252
    /**
253
     * Update post meta for the given post id
254
     * 
255
     * @param type $final_instance
256
     * @param type $post_id
257
     */
258
    private function update_post_meta( $final_instance, $post_id )
259
    {
260
        foreach( $final_instance as $name => $value )
261
        {
262
            \update_post_meta( $post_id, $name, $value );
263
        }
264
    }
265
    
266
    /**
267
     * Get existing meta values from the database.
268
     * 
269
     * @param array $metabox
270
     * @param number $post_id
271
     * @return array
272
     */
273
    private function get_old_instance( $metabox, $post_id )
274
    {
275
        $old_instance = array();
276
        
277
        foreach( $metabox['fields'] as $field )
278
        {
279
            if( in_array($field['name'], get_post_custom_keys($post_id)) )
280
            {
281
                $old_instance[$field['name']] = \get_post_meta( $post_id, $field['name'], true );
282
            }
283
        }
284
        
285
        return $old_instance;
286
    }
287
    
288
    /**
289
     * Default arguments for the add() method.
290
     * 
291
     * @return array
292
     */
293
    private function default_args()
294
    {
295
        return array(
296
            'title'    => null,
297
            'screen'   => null,
298
            'context'  => 'advanced',
299
            'priority' => 'default',
300
            'fields'   => array()
301
        );
302
    }
303
}