Advertisement
  1. Code
  2. WordPress
  3. Plugin Development

Tabel Database kustom: Membuat API

Scroll to top
Read Time: 15 min
This post is part of a series called Custom Database Tables.
Custom Database Tables: Safety First
Custom Database Tables: Maintaining the Database

() translation by (you can also view the original English article)

Dalam bagian pertama dari seri ini kita melihat kelemahan menggunakan meja kustom. Salah satu yang utama adalah kurangnya API: Jadi dalam artikel ini kita akan melihat bagaimana untuk membuat satu. API bertindak lapisan antara penanganan data dalam plug-in dan interaksi sebenarnya dengan tabel database- dan terutama ditujukan untuk memastikan interaksi tersebut aman dan memberikan 'ramah manusia' pembungkus untuk meja Anda. Dengan demikian kita akan memerlukan pembungkus fungsi untuk memasukkan, update, menghapus dan query data.


Mengapa saya harus membuat sebuah API?

Ada beberapa alasan mengapa API dianjurkan – tetapi sebagian mendidih ke dua prinsip yang terkait: mengurangi duplikasi kode dan keprihatinan pemisahan.

Lebih aman

Dengan bungkusnya disebutkan di atas empat fungsi Anda hanya perlu memastikan Anda query database aman di empat tempat – Anda kemudian bisa melupakan sanitisation sepenuhnya. Setelah Anda yakin bahwa fungsi pembungkus Anda menangani database dengan aman, maka Anda tidak perlu khawatir tentang data Anda kepada mereka. Anda juga dapat memvalidasi data – kembali kesalahan jika sesuatu yang tidak benar.

Idenya adalah bahwa tanpa fungsi ini Anda harus memastikan bahwa setiap contoh berinteraksi dengan database Anda melakukannya dengan aman. Ini hanya membawa kemungkinan peningkatan bahwa dalam salah satu kasus ini Anda akan kehilangan sesuatu dan membuat kerentanan dalam plug-in.

Mengurangi Bugs

Hal ini terkait dengan titik pertama (dan keduanya berkaitan dengan duplikasi kode). Oleh duplikasi code ada cakupan yang lebih besar untuk bug merangkak di. Sebaliknya, dengan menggunakan fungsi pembungkus-jika ada bug dengan memperbarui atau query tabel database-Anda tahu persis di mana untuk melihat.

Lebih mudah untuk dibaca

Ini mungkin tampak seperti alasan 'lembut'- tapi mudah dibaca kode sangat penting. Pembacaan adalah tentang membuat logika dan tindakan kode yang jelas kepada pembaca. Ini tidak hanya penting ketika bekerja sebagai bagian dari tim, atau ketika seseorang dapat mewarisi pekerjaan Anda: Anda mungkin tahu apa kode Anda dimaksudkan untuk lakukan sekarang, tapi dalam enam bulan Anda akan mungkin lupa. Dan jika kode Anda sulit untuk mengikuti, lebih mudah untuk memperkenalkan bug.

Pembungkus fungsi membersihkan kode Anda dengan harfiah memisahkan kerja internal beberapa operasi (katakanlah membuat posting) dari konteks bahwa operasi (katakanlah penanganan pengiriman form). Bayangkan memiliki seluruh isi wp_insert_post() tempat setiap contoh Anda menggunakan wp_insert_post().

Menambahkan lapisan abstraksi

Menambahkan lapisan abstraksi ini tidak selalu hal yang baik- tapi di sini itu tidak diragukan lagi. Tidak hanya pembungkus ini menyediakan cara ramah manusia memperbarui atau query tabel (bayangkan harus menggunakan SQL untuk posting pertanyaan daripada menggunakan WP_Query() jauh lebih ringkas- dan semua SQL perumusan dan sanitisation yang terjadi dengan itu), tetapi juga membantu melindungi Anda dan pengembang lain dari perubahan struktur database yang mendasari.

Dengan menggunakan fungsi pembungkus Anda, serta pihak ketiga, dapat menggunakan ini tanpa takut bahwa mereka tidak aman atau akan pecah. Jika Anda memutuskan untuk mengubah nama kolom, memindahkan kolom di tempat lain atau bahkan menghapus itu Anda dapat yakin bahwa sisa plug-in tidak akan pecah, karena Anda hanya membuat perubahan yang diperlukan untuk fungsi pembungkus Anda. (Kebetulan ini adalah alasan kuat untuk menghindari langsung SQL query tabel WordPress: jika mereka mengubah, dan mereka akan, itu akan pecah plug-in.). Di sisi lain API membantu plug-in dilanjutkan di dalam cara yang stabil.

Konsistensi

Aku mungkin bersalah membelah satu titik ke dua di sini - tetapi saya merasa bahwa ini manfaat penting. Ada sedikit lebih buruk daripada inkonsistensi ketika mengembangkan plug-in: hanya mendorong kode berantakan. Pembungkus fungsi memberikan interaksi yang konsisten dengan database: Anda memberikan data dan kembali benar (atau ID) atau palsu (atau objek WP_Error, jika Anda lebih suka).


API

Mudah-mudahan saya sekarang yakin tentang perlunya sebuah API untuk meja Anda. Tapi sebelum kita melangkah lebih jauh kita pertama akan mendefinisikan fungsi pembantu yang akan membuat sanitisation agak mudah.

Kolom Table

Kita akan mendefinisikan sebuah fungsi yang mengembalikan kolom tabel dengan format data yang mereka harapkan. Dengan melakukan ini kita dapat dengan mudah whitelist diperbolehkan kolom dan format input yang sesuai. Selain itu jika kita membuat perubahan ke kolom, kita hanya perlu membuat perubahan di sini

1
function wptuts_get_log_table_columns(){
2
    return array(
3
        'log_id'=> '%d',
4
        'user_id'=> '%d',
5
        'activity'=>'%s',
6
        'object_id'=>'%d',
7
        'object_type'=>'%s',
8
        'activity_date'=>'%s',
9
    );
10
}

Memasukkan Data

Fungsi pembungkus 'insert' paling mendasar hanya akan mengambil array nilai kolom pasangan dan masukkan ini ke database. Ini tidak perlu kasus: Anda dapat memutuskan untuk menyediakan lebih banyak 'ramah manusia' tombol yang Anda kemudian peta untuk nama-nama kolom. Anda mungkin juga memutuskan bahwa beberapa nilai-nilai dihasilkan otomatis atau over Berkuda berdasarkan nilai yang dikirimkan (misalnya: posting status di wp_insert_post()).

Itu mungkin * nilai * yang perlu pemetaan. Format yang terbaik disimpan dalam hal tidak selalu format yang paling nyaman untuk menggunakan. Misalnya, untuk tanggal itu mungkin lebih mudah untuk menangani objek DateTime atau timestamp - dan kemudian mengubah ini untuk format tanggal yang diinginkan.

Fungsi pembungkus mungkin sederhana atau rumit – tetapi minimum yang harus dilakukan adalah sanitise masukan. Saya juga merekomendasikan membolehkan akses untuk kolom diakui, sebagai mencoba untuk memasukkan data ke dalam kolom yang tidak ada bisa melempar kesalahan.

Dalam contoh ini adalah ID pengguna dengan default yang pengguna saat ini, dan semua bidang yang diberikan oleh nama kolom mereka - yang pengecualian kegiatan yang dilewatkan sebagai 'date'. Tanggal, dalam contoh ini, harus timestamp lokal, yang dikonversi sebelum menambahkannya ke database.

1
/**

2
 * Inserts a log into the database

3
 *

4
 *@param $data array An array of key => value pairs to be inserted

5
 *@return int The log ID of the created activity log. Or WP_Error or false on failure.

6
*/
7
function wptuts_insert_log( $data=array() ){
8
    global $wpdb;        
9
10
    //Set default values

11
    $data = wp_parse_args($data, array(
12
                 'user_id'=> get_current_user_id(),
13
                 'date'=> current_time('timestamp'),
14
    ));    
15
16
    //Check date validity

17
    if( !is_float($data['date']) || $data['date'] <= 0 )
18
        return 0;
19
20
    //Convert activity date from local timestamp to GMT mysql format

21
    $data['activity_date'] = date_i18n( 'Y-m-d H:i:s', $data['date'], true );
22
23
    //Initialise column format array

24
    $column_formats = wptuts_get_log_table_columns();
25
26
    //Force fields to lower case

27
    $data = array_change_key_case ( $data );
28
29
    //White list columns

30
    $data = array_intersect_key($data, $column_formats);
31
32
    //Reorder $column_formats to match the order of columns given in $data

33
    $data_keys = array_keys($data);
34
    $column_formats = array_merge(array_flip($data_keys), $column_formats);
35
36
    $wpdb->insert($wpdb->wptuts_activity_log, $data, $column_formats);
37
38
    return $wpdb->insert_id;
39
}
Tip: Hal ini juga ide yang baik untuk memeriksa validitas data. Apa cek Anda harus melakukan, dan bagaimana bereaksi API, sepenuhnya tergantung pada konteks Anda. wp_insert_post(), untuk contoh memerlukan tingkat tertentu keunikan untuk pos slugs - jika ada bentrokan, ini otomatis menghasilkan satu yang unik. wp_insert_term di sisi lain kembali kesalahan jika istilah yang sudah ada. Hal ini ke campuran antara cara WordPress menangani objek dan semantik ini.

Memperbarui Data

Memperbarui data biasanya erat meniru memasukkan data – dengan pengecualian bahwa sebuah baris identifier (biasanya hanya primary key) disediakan bersama dengan data yang perlu diperbarui. Secara umum argumen harus sesuai fungsi insert (untuk konsistensi) - jadi dalam contoh ini, 'tanggal' digunakan 'activity_date'

1
/**

2
 * Updates an activity log with supplied data

3
 *

4
 *@param $log_id int ID of the activity log to be updated

5
 *@param $data array An array of column=>value pairs to be updated

6
 *@return bool Whether the log was successfully updated.

7
*/
8
function wptuts_update_log( $log_id, $data=array() ){
9
    global $wpdb;        
10
11
    //Log ID must be positive integer

12
    $log_id = absint($log_id);     
13
    if( empty($log_id) )
14
         return false;
15
16
    //Convert activity date from local timestamp to GMT mysql format

17
    if( isset($data['activity_date']) )
18
         $data['activity_date'] = date_i18n( 'Y-m-d H:i:s', $data['date'], true );
19
20
21
    //Initialise column format array

22
    $column_formats = wptuts_get_log_table_columns();
23
24
    //Force fields to lower case

25
    $data = array_change_key_case ( $data );
26
27
    //White list columns

28
    $data = array_intersect_key($data, $column_formats);
29
30
    //Reorder $column_formats to match the order of columns given in $data

31
    $data_keys = array_keys($data);
32
    $column_formats = array_merge(array_flip($data_keys), $column_formats);
33
34
    if ( false === $wpdb->update($wpdb->wptuts_activity_log, $data, array('log_id'=>$log_id), $column_formats) ) {
35
         return false;
36
    }
37
38
    return true;
39
}

Data query

Fungsi pembungkus untuk query data sering akan cukup rumit – terutama karena Anda mungkin ingin mendukung semua jenis pertanyaan yang memilih hanya bidang-bidang tertentu yang membatasi dengan dan atau pernyataan OR, memesan dengan salah satu dari beberapa kolom mungkin dll (hanya Lihat WP_ Query kelas).

Prinsip dasar dari pembungkus fungsi untuk query data adalah bahwa harus harus mengambil sebuah 'permintaan array', menafsirkan, dan membentuk pernyataan SQL yang sesuai.

1
/**

2
 * Retrieves activity logs from the database matching $query.

3
 * $query is an array which can contain the following keys:

4
 *

5
 * 'fields' - an array of columns to include in returned roles. Or 'count' to count rows. Default: empty (all fields).

6
 * 'orderby' - datetime, user_id or log_id. Default: datetime.

7
 * 'order' - asc or desc

8
 * 'user_id' - user ID to match, or an array of user IDs

9
 * 'since' - timestamp. Return only activities after this date. Default false, no restriction.

10
 * 'until' - timestamp. Return only activities up to this date. Default false, no restriction.

11
 *

12
 *@param $query Query array

13
 *@return array Array of matching logs. False on error.

14
*/
15
function wptuts_get_logs( $query=array() ){
16
17
     global $wpdb;
18
     /* Parse defaults */
19
     $defaults = array(
20
       'fields'=>array(),'orderby'=>'datetime','order'=>'desc', 'user_id'=>false,
21
       'since'=>false,'until'=>false,'number'=>10,'offset'=>0
22
     );
23
24
    $query = wp_parse_args($query, $defaults);
25
26
    /* Form a cache key from the query */
27
    $cache_key = 'wptuts_logs:'.md5( serialize($query));
28
    $cache = wp_cache_get( $cache_key );
29
30
    if ( false !== $cache ) {
31
            $cache = apply_filters('wptuts_get_logs', $cache, $query);
32
            return $cache;
33
    }
34
35
     extract($query);
36
37
    /* SQL Select */
38
    //Whitelist of allowed fields

39
    $allowed_fields = wptuts_get_log_table_columns();
40
  
41
    if( is_array($fields) ){
42
	    //Convert fields to lowercase (as our column names are all lower case - see part 1)

43
	    $fields = array_map('strtolower',$fields);
44
45
    	    //Sanitize by white listing

46
    	   $fields = array_intersect($fields, $allowed_fields);
47
	}else{
48
		$fields = strtolower($fields);
49
	}
50
51
    //Return only selected fields. Empty is interpreted as all

52
    if( empty($fields) ){
53
        $select_sql = "SELECT* FROM {$wpdb->wptuts_activity_log}";
54
    }elseif( 'count' == $fields ) {
55
        $select_sql = "SELECT COUNT(*) FROM {$wpdb->wptuts_activity_log}";
56
    }else{
57
        $select_sql = "SELECT ".implode(',',$fields)." FROM {$wpdb->wptuts_activity_log}";
58
    }
59
60
     /*SQL Join */
61
     //We don't need this, but we'll allow it be filtered (see 'wptuts_logs_clauses' )

62
     $join_sql='';
63
64
    /* SQL Where */
65
    //Initialise WHERE

66
    $where_sql = 'WHERE 1=1';
67
68
    if( !empty($log_id) )
69
       $where_sql .=  $wpdb->prepare(' AND log_id=%d', $log_id);
70
71
    if( !empty($user_id) ){
72
73
       //Force $user_id to be an array

74
       if( !is_array( $user_id) )
75
           $user_id = array($user_id);
76
77
       $user_id = array_map('absint',$user_id); //Cast as positive integers

78
       $user_id__in = implode(',',$user_id);
79
       $where_sql .=  " AND user_id IN($user_id__in)";
80
    }
81
82
    $since = absint($since);
83
    $until = absint($until);
84
85
    if( !empty($since) )
86
       $where_sql .=  $wpdb->prepare(' AND activity_date >= %s', date_i18n( 'Y-m-d H:i:s', $since, true));
87
88
    if( !empty($until) )
89
       $where_sql .=  $wpdb->prepare(' AND activity_date <= %s', date_i18n( 'Y-m-d H:i:s', $until, true));
90
91
    /* SQL Order */
92
    //Whitelist order

93
    $order = strtoupper($order);
94
    $order = ( 'ASC' == $order ? 'ASC' : 'DESC' );
95
96
    switch( $orderby ){
97
       case 'log_id':
98
            $order_sql = "ORDER BY log_id $order";
99
       break;
100
       case 'user_id':
101
            $order_sql = "ORDER BY user_id $order";
102
       break;
103
       case 'datetime':
104
             $order_sql = "ORDER BY activity_date $order";
105
       default:
106
       break;
107
    }
108
109
    /* SQL Limit */
110
    $offset = absint($offset); //Positive integer

111
    if( $number == -1 ){
112
         $limit_sql = "";
113
    }else{
114
         $number = absint($number); //Positive integer

115
         $limit_sql = "LIMIT $offset, $number";
116
    }
117
118
    /* Filter SQL */
119
    $pieces = array( 'select_sql', 'join_sql', 'where_sql', 'order_sql', 'limit_sql' );
120
    $clauses = apply_filters( 'wptuts_logs_clauses', compact( $pieces ), $query );
121
    foreach ( $pieces as $piece )
122
          $$piece = isset( $clauses[ $piece ] ) ? $clauses[ $piece ] : '';
123
124
    /* Form SQL statement */
125
    $sql = "$select_sql $where_sql $order_sql $limit_sql";
126
127
    if( 'count' == $fields ){
128
        return $wpdb->get_var($sql);
129
    }
130
131
    /* Perform query */
132
    $logs = $wpdb->get_results($sql);
133
134
    /* Add to cache and filter */
135
    wp_cache_add( $cache_key, $logs, 24*60*60 );
136
    $logs = apply_filters('wptuts_get_logs', $logs, $query);
137
    return $logs;
138
 }

Ada sedikit yang adil yang terjadi dalam contoh di atas sebagai aku sudah mencoba untuk menyertakan fitur beberapa yang mungkin dianggap ketika mengembangkan fungsi pembungkus Anda, yang kita bahas dalam bagian berikutnya.

Cache

Anda dapat mempertimbangkan pertanyaan Anda menjadi cukup kompleks, atau berulang-ulang secara teratur, yang masuk akal untuk men-cache hasil. Karena berbeda query akan mengembalikan hasil yang berbeda, kami jelas tidak ingin menggunakan kunci generik cache-kita perlu satu yang unik untuk kueri tersebut. Ini adalah persis apa berikut dilakukan. Serialises array permintaan, dan kemudian hash itu, memproduksi kunci unik untuk $query:

1
 $cache_key = 'wptuts_logs:'.md5( serialize($query));

Selanjutnya kami memeriksa jika kita memiliki sesuatu yang disimpan untuk bahwa cache kunci-jika demikian, besar, kami hanya mengembalikan isinya. Jika tidak, kami menghasilkan SQL, melakukan query dan kemudian menambah hasil cache (untuk paling 24 jam) dan mengembalikan mereka. Kita harus ingat bahwa catatan bisa memakan waktu hingga 24 jam untuk muncul dalam hasil dari fungsi ini. Biasanya ada konteks mana cache secara otomatis dihapus - tetapi kita akan perlu untuk mengimplementasikan ini.

Filter & tindakan

Kait telah dibahas secara luas di WPTuts + baru-baru ini oleh Tom McFarlin dan Pippin Williamson. Dalam artikelnya, Pippin berbicara tentang alasan mengapa Anda harus membuat kode Anda extensible melalui kait, dan pembungkus seperti wptuts_get_logs() berfungsi sebagai contoh yang sangat baik dari mana mereka dapat digunakan.

Kami telah menggunakan dua filter dalam fungsi di atas:

  • wptuts_get_logs – filter hasil dari fungsi
  • wptuts_logs_clauses – filter array SQL komponen

Hal ini memungkinkan pengembang pihak ketiga, atau bahkan diri kita sendiri, untuk membangun API disediakan. Jika kita menghindari penggunaan SQL langsung di kami plug-in dan hanya menggunakan fungsi pembungkus yang kami buat, maka itu segera membuat mungkin untuk memperpanjang kami plug-in. Wptuts_logs_clauses filter khususnya akan memungkinkan pengembang untuk mengubah setiap bagian dari SQL- dan dengan demikian melakukan query yang kompleks. Kita akan perhatikan bahwa itu adalah pekerjaan setiap plug-in menggunakan filter ini untuk memastikan bahwa apa yang mereka kembali adalah benar orang minta-minga.

Kait hanya berguna ketika melakukan tiga utama 'operasi lainnya': memasukkan, memperbarui, dan menghapus data. Tindakan memungkinkan plug-in untuk tahu Kapan ini sedang dilakukan – sehingga mereka beberapa tindakan. Dalam konteks kami ini mungkin berarti mengirimkan email ke admin ketika pengguna tertentu melakukan tindakan tertentu. Filter, dalam konteks operasi ini, berguna untuk mengubah data sebelum yang dimasukkan.

Berhati-hatilah ketika penamaan kait. Nama baik hook melakukan beberapa hal:

  • Berkomunikasi Kapan hook disebut atau apa yang mereka lakukan (misalnya Anda bisa menebak apa yang pre_get_posts dan user_has_cap akan dilakukan.
  • Jadilah unik. Yang dianjurkan Anda awalan kait dengan nama Anda plug-di itu. Tidak seperti fungsi, tidak akan ada kesalahan jika ada bentrokan antara nama hook-sebaliknya itu mungkin hanya 'diam' akan melanggar satu atau lebih plug-in.
  • Pameran semacam struktur. Membuat kait Anda predicable, dan menghindari penamaan kait 'on the fly', karena hal ini kadang-kadang dapat menyebabkan nama kait tampaknya acak. Malah merencanakan sejauh mungkin kait Anda akan menggunakan, dan datang dengan konvensi penamaan yang sesuai – dan menaatinya.
Tip: Biasanya itu adalah ide yang baik untuk meniru Konvensi sama seperti WordPress – sebagai pengembang akan lebih cepat mengerti apa hook itu lakukan. Mengenai menggunakan plug-di itu nama sebagai awalan: jika Anda plug-in nama generik maka ini mungkin tidak cukup untuk memastikan keunikan. Akhirnya, tidak memberikan tindakan dan filter nama yang sama.

Menghapus Data

Menghapus data adalah sering yang paling sederhana dari pembungkus-meskipun itu mungkin diperlukan untuk melakukan beberapa 'clean up' operasi serta hanya menghapus data. wp_delete_post() misalnya, tidak hanya menghapus pos dari * _posts meja tapi juga menghapus yang sesuai posting meta, taksonomi hubungan, komentar dan revisi dll.

Sesuai dengan komentar-komentar dari bagian sebelumnya, kami akan memasukkan dua dua tindakan: satu dipicu sebelum dan yang lain setelah log telah dihapus dari tabel. Berikut WordPress' konvensi penamaan untuk tindakan tersebut:

  • _delete_ dipicu sebelum penghapusan
  • _deleted_ dipicu setelah penghapusan
1
/**

2
 * Deletes an activity log from the database

3
 *

4
 *@param $log_id int ID of the activity log to be deleted

5
 *@return bool Whether the log was successfully deleted.

6
*/
7
function wptuts_delete_log( $log_id ){
8
    global $wpdb;        
9
10
    //Log ID must be positive integer

11
    $log_id = absint($log_id);     
12
    if( empty($log_id) )
13
         return false;
14
15
    do_action('wptuts_delete_log',$log_id);
16
    $sql = $wpdb->prepare("DELETE from {$wpdb->wptuts_activity_log} WHERE log_id = %d", $log_id);
17
18
    if( !$wpdb->query( $sql ) )
19
         return false;
20
21
    do_action('wptuts_deleted_log',$log_id);
22
23
    return true;
24
}

Dokumentasi

Aku sudah sedikit malas dengan dokumentasi di sumber api di atas. Dalam seri ini Tom McFarlin menjelaskan mengapa Anda tidak boleh. Anda mungkin telah menghabiskan banyak waktu untuk mengembangkan fungsi API Anda, tetapi jika pengembang lain tidak tahu bagaimana menggunakan mereka - mereka tidak akan. Anda akan juga membantu diri sendiri, ketika setelah 6 bulan Anda lupa bagaimana data harus diberikan, atau apa yang harus Anda harapkan untuk dikembalikan.


Ringkasan

Pembungkus untuk tabel database Anda dapat berkisar dari yang relatif sederhana (misalnya get_terms()) untuk sangat kompleks (misalnya kelas WP_Query). Secara kolektif mereka harus berusaha untuk melayani sebagai pintu gerbang ke meja Anda: memungkinkan Anda untuk fokus pada konteks di mana mereka digunakan, dan pada dasarnya lupa apa yang mereka benar-benar lakukan. API yang Anda buat adalah contoh kecil dari gagasan tentang 'keprihatinan pemisahan', sering dikaitkan dengan Edsger W. Dijkstra dalam makalahnya peran pemikiran ilmiah:

Ini adalah apa saya kadang-kadang disebut "keprihatinan pemisahan", yang, bahkan jika tidak sempurna mungkin, adalah teknik yang hanya tersedia untuk memesan efektif pikiran seseorang, yang saya tahu. Ini adalah apa yang saya maksud dengan "memfokuskan perhatian pada beberapa aspek": itu berarti mengabaikan aspek-aspek lain, itu hanya melakukan keadilan dengan fakta bahwa dari aspek ini, sudut pandang, yang lain tidak relevan. Ini menjadi satu dan multi-jalur berpikiran secara bersamaan.

Anda dapat menemukan kode yang digunakan dalam seri ini, secara keseluruhan, di GitHub. Dalam bagian selanjutnya dari seri ini kita akan melihat bagaimana Anda dapat menjaga database Anda, dan menangani upgrade.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.