Completed
Push — master ( 69a7c5...7cc7cb )
by Askupa
01:55
created

Manager   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 319
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 319
rs 8.8
c 0
b 0
f 0
wmc 36
lcom 2
cbo 1

16 Methods

Rating   Name   Duplication   Size   Complexity  
A get_instance() 0 8 2
A __construct() 0 4 1
A add() 0 9 2
A render() 0 14 1
A add_meta_boxes() 0 14 2
A save_meta_boxes() 0 12 3
A save_meta_box() 0 14 3
A get_meta_box_value() 0 12 2
A print_style() 0 15 3
A print_errors() 0 15 3
A init() 0 6 1
A update_form() 0 18 2
A update_post_meta() 0 7 2
B can_save() 0 20 5
A get_old_instance() 0 14 3
A default_args() 0 10 1
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 ) 
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
33
        {
34
            static::$instance = new static();
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
Documentation Bug introduced by
It seems like new static() of type this<Amarkal\Metabox\Manager> is incompatible with the declared type object<Amarkal\Metabox\Singleton> of property $instance.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
35
        }
36
        return static::$instance;
0 ignored issues
show
Bug introduced by
Since $instance is declared private, accessing it with static will lead to errors in possible sub-classes; consider using self, or increasing the visibility of $instance to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return static::$someVariable;
    }
}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass { }

YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class SomeClass
{
    private static $someVariable;

    public static function getSomeVariable()
    {
        return self::$someVariable; // self works fine with private.
    }
}
Loading history...
Bug Compatibility introduced by
The expression static::$instance; of type Amarkal\Metabox\Manager|Amarkal\Metabox\Singleton adds the type Amarkal\Metabox\Manager to the return on line 36 which is incompatible with the return type documented by Amarkal\Metabox\Manager::get_instance of type Amarkal\Metabox\Singleton.
Loading history...
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
        include __DIR__.'/Form.phtml';
83
    }
84
    
85
    /**
86
     * Internally used to register metaboxes.
87
     */
88
    public function add_meta_boxes()
89
    {
90
        foreach( $this->metaboxes as $id => $args )
91
        {
92
            \add_meta_box(
93
                $id,
94
                $args['title'],
95
                array($this, 'render'),
96
                $args['screen'],
97
                $args['context'],
98
                $args['priority']
99
            );
100
        }
101
    }
102
    
103
    /**
104
     * Save metaboxes data for a given page.
105
     * 
106
     * @param number $post_id
107
     */
108
    public function save_meta_boxes( $post_id )
109
    {
110
        if($this->can_save($post_id))
111
        {
112
            // Update the meta fields.
113
            foreach( $this->metaboxes as $id => $metabox )
114
            {
115
                $this->save_meta_box( $post_id, $id, $metabox );
116
            }
117
        }
118
        return $post_id;
119
    }
120
    
121
    /**
122
     * Save the data of a single metabox.
123
     * 
124
     * @param number $post_id
125
     * @param string $id
126
     * @param array $metabox
127
     */
128
    public function save_meta_box( $post_id, $id, $metabox )
129
    {
130
        $nonce_name   = $id.'_nonce';
131
        $nonce_value  = filter_input(INPUT_POST, $nonce_name);
132
        $new_instance = filter_input_array(INPUT_POST);
133
        
134
        // Check if our nonce is set and verify it
135
        if( null === $nonce_value || !wp_verify_nonce($nonce_value, self::NONCE_ACTION) ) 
136
        {
137
            return $post_id;
138
        }
139
140
        $this->update_form($metabox, $post_id, $new_instance);
141
    }
142
    
143
    /**
144
     * Get the value of the given field.
145
     * 
146
     * @param string $metabox_id
147
     * @param string $name
148
     * @param number $post_id
149
     * @return mix
150
     */
151
    public function get_meta_box_value( $metabox_id, $name, $post_id )
152
    {
153
        // Check if the meta key exists
154
        if( in_array($name, get_post_custom_keys($post_id)) )
155
        {
156
            return get_post_meta( $post_id, $name, true );
157
        }
158
        
159
        // If no meta key exists in the db, use default value
160
        $component = $this->metaboxes[$metabox_id]['form']->get_component($name);
161
        return $component->default;
162
    }
163
    
164
    /**
165
     * Print custom metabox style.
166
     */
167
    public function print_style() 
168
    {
169
        $cs = get_current_screen();
170
        
171
        foreach( $this->metaboxes as $metabox )
172
        {
173
            if( $metabox['screen'] === $cs->id )
174
            {
175
                echo '<style>';
176
                include 'metabox.css';
177
                echo '</style>';
178
                return;
179
            }
180
        }
181
    }
182
    
183
    /**
184
     * Print all errors stored in a transient for a given post ID.
185
     * 
186
     * @param number $post_id
187
     * @param array $metabox
188
     */
189
    public function print_errors( $post_id, $metabox )
190
    {
191
        $errors  = \get_transient("amarkal_metabox_errors_$post_id");
192
        
193
        if( $errors )
194
        {
195
            foreach( $errors as $name => $error )
196
            {
197
                $component = $metabox['form']->get_component($name);
198
                echo "<div class=\"notice notice-error\"><p><strong>{$component->title}</strong> $error</p></div>";
199
            }
200
        }
201
        
202
        \delete_transient("amarkal_metabox_errors_$post_id");
203
    }
204
    
205
    /**
206
     * Initiate the metaboxes by adding action hooks for printing and saving.
207
     */
208
    private function init()
209
    {
210
        \add_action( 'add_meta_boxes', array( $this, 'add_meta_boxes' ) );
211
        \add_action( 'save_post', array( $this, 'save_meta_boxes' ) );
212
        \add_action( 'admin_footer', array( $this, 'print_style' ) );
213
    }
214
    
215
    /**
216
     * Update the form data for the given given metabox. If the $new_instance
217
     * contains new data, it will be saved into the db and if there are any 
218
     * validation errors they will be printed.
219
     * 
220
     * @param array $metabox
221
     * @param number $post_id
222
     * @param array $new_instance
223
     */
224
    private function update_form( $metabox, $post_id, array $new_instance = array() )
225
    {
226
        $old_instance   = $this->get_old_instance($metabox, $post_id);
227
        $final_instance = $metabox['form']->update( $new_instance, $old_instance );
228
229
        // Update db if there is new data to be saved
230
        if( array() !== $new_instance )
231
        {
232
            $this->update_post_meta($final_instance, $post_id);
0 ignored issues
show
Documentation introduced by
$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...
233
            
234
            /**
235
             * We need to store all errors in a transient since WordPress does
236
             * a redirect to post.php and then back to our post, which clears
237
             * the execution thread. See https://www.sitepoint.com/displaying-errors-from-the-save_post-hook-in-wordpress/
238
             */
239
            \set_transient( "amarkal_metabox_errors_$post_id", $metabox['form']->get_errors(), 60 );
240
        }
241
    }
242
    
243
    /**
244
     * Update post meta for the given post id
245
     * 
246
     * @param type $final_instance
247
     * @param type $post_id
248
     */
249
    private function update_post_meta( $final_instance, $post_id )
250
    {
251
        foreach( $final_instance as $name => $value )
252
        {
253
            \update_post_meta( $post_id, $name, $value );
254
        }
255
    }
256
    
257
    /**
258
     * A note on security:
259
     * 
260
     * We need to verify this came from the our screen and with proper authorization,
261
     * because save_post can be triggered at other times. since metaboxes can 
262
     * be removed - by having a nonce field in only one metabox there is no 
263
     * guarantee the nonce will be there. By placing a nonce field in each 
264
     * metabox you can check if data from that metabox has been sent 
265
     * (and is actually from where you think it is) prior to processing any data.
266
     * @see http://wordpress.stackexchange.com/a/49460/25959
267
     */
268
    private function can_save( $post_id )
269
    {
270
        /*
271
         * If this is an autosave, our form has not been submitted,
272
         * so we don't want to do anything.
273
         */
274
        if( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) 
275
        {
276
            return false;
277
        }
278
279
        // Check the user's permissions.
280
        $post_type = filter_input(INPUT_POST, 'post_type');
281
        if( null !== $post_type && !current_user_can('edit_'.$post_type, $post_id) )
282
        {
283
            return false;
284
        }
285
        
286
        return true;
287
    }
288
    
289
    /**
290
     * Get existing meta values from the database.
291
     * 
292
     * @param array $metabox
293
     * @param number $post_id
294
     * @return array
295
     */
296
    private function get_old_instance( $metabox, $post_id )
297
    {
298
        $old_instance = array();
299
        
300
        foreach( $metabox['fields'] as $field )
301
        {
302
            if( in_array($field['name'], get_post_custom_keys($post_id)) )
303
            {
304
                $old_instance[$field['name']] = \get_post_meta( $post_id, $field['name'], true );
305
            }
306
        }
307
        
308
        return $old_instance;
309
    }
310
    
311
    /**
312
     * Default arguments for the add() method.
313
     * 
314
     * @return array
315
     */
316
    private function default_args()
317
    {
318
        return array(
319
            'title'    => null,
320
            'screen'   => null,
321
            'context'  => 'advanced',
322
            'priority' => 'default',
323
            'fields'   => array()
324
        );
325
    }
326
}