Issues (2882)

src/Dev/BulkLoader.php (1 issue)

Severity
1
<?php
2
3
namespace SilverStripe\Dev;
4
5
use SilverStripe\Core\Environment;
6
use SilverStripe\ORM\DataObject;
7
use SilverStripe\View\ViewableData;
8
9
/**
10
 * A base for bulk loaders of content into the SilverStripe database.
11
 * Bulk loaders give SilverStripe authors the ability to do large-scale uploads into their SilverStripe databases.
12
 *
13
 * You can configure column-handling,
14
 *
15
 * @todo Add support for adding/editing has_many relations.
16
 * @todo Add support for deep chaining of relation properties (e.g. Player.Team.Stats.GoalCount)
17
 * @todo Character conversion
18
 *
19
 * @see http://tools.ietf.org/html/rfc4180
20
 * @author Ingo Schommer, Silverstripe Ltd. (<firstname>@silverstripe.com)
21
 */
22
abstract class BulkLoader extends ViewableData
23
{
24
25
    /**
26
     * Each row in the imported dataset should map to one instance
27
     * of this class (with optional property translation
28
     * through {@self::$columnMaps}.
29
     *
30
     * @var string
31
     */
32
    public $objectClass;
33
34
    /**
35
     * Override this on subclasses to give the specific functions names.
36
     *
37
     * @var string
38
     */
39
    public static $title;
40
41
    /**
42
     * Map columns to DataObject-properties.
43
     * If not specified, we assume the first row
44
     * in the file contains the column headers.
45
     * The order of your array should match the column order.
46
     *
47
     * The column count should match the count of array elements,
48
     * fill with NULL values if you want to skip certain columns.
49
     *
50
     * You can also combine {@link $hasHeaderRow} = true and {@link $columnMap}
51
     * and omit the NULL values in your map.
52
     *
53
     * Supports one-level chaining of has_one relations and properties with dot notation
54
     * (e.g. Team.Title). The first part has to match a has_one relation name
55
     * (not necessarily the classname of the used relation).
56
     *
57
     * <code>
58
     * <?php
59
     *  // simple example
60
     *  array(
61
     *      'Title',
62
     *      'Birthday'
63
     *  )
64
     *
65
     * // complex example
66
     *  array(
67
     *      'first name' => 'FirstName', // custom column name
68
     *      null, // ignored column
69
     *      'RegionID', // direct has_one/has_many ID setting
70
     *      'OrganisationTitle', // create has_one relation to existing record using $relationCallbacks
71
     *      'street' => 'Organisation.StreetName', // match an existing has_one or create one and write property.
72
     *  );
73
     * ?>
74
     * </code>
75
     *
76
     * @var array
77
     */
78
    public $columnMap = array();
79
80
    /**
81
     * Find a has_one relation based on a specific column value.
82
     *
83
     * <code>
84
     * <?php
85
     * array(
86
     *      'OrganisationTitle' => array(
87
     *          'relationname' => 'Organisation', // relation accessor name
88
     *          'callback' => 'getOrganisationByTitle',
89
     *      );
90
     * );
91
     * ?>
92
     * </code>
93
     *
94
     * @var array
95
     */
96
    public $relationCallbacks = array();
97
98
    /**
99
     * Specifies how to determine duplicates based on one or more provided fields
100
     * in the imported data, matching to properties on the used {@link DataObject} class.
101
     * Alternatively the array values can contain a callback method (see example for
102
     * implementation details). The callback method should be defined on the source class.
103
     *
104
     * NOTE: If you're trying to get a unique Member record by a particular field that
105
     * isn't Email, you need to ensure that Member is correctly set to the unique field
106
     * you want, as it will merge any duplicates during {@link Member::onBeforeWrite()}.
107
     *
108
     * {@see Member::$unique_identifier_field}.
109
     *
110
     * If multiple checks are specified, the first non-empty field "wins".
111
     *
112
     *  <code>
113
     * <?php
114
     * array(
115
     *      'customernumber' => 'ID',
116
     *      'phonenumber' => array(
117
     *          'callback' => 'getByImportedPhoneNumber'
118
     *      )
119
     * );
120
     * ?>
121
     * </code>
122
     *
123
     * @var array
124
     */
125
    public $duplicateChecks = array();
126
127
    /**
128
     * @var Boolean $clearBeforeImport Delete ALL records before importing.
129
     */
130
    public $deleteExistingRecords = false;
131
132
    public function __construct($objectClass)
133
    {
134
        $this->objectClass = $objectClass;
135
        parent::__construct();
136
    }
137
138
    /*
139
     * Load the given file via {@link self::processAll()} and {@link self::processRecord()}.
140
     * Optionally truncates (clear) the table before it imports.
141
     *
142
     * @return BulkLoader_Result See {@link self::processAll()}
143
     */
144
    public function load($filepath)
145
    {
146
        Environment::increaseTimeLimitTo(3600);
147
        Environment::increaseMemoryLimitTo('512M');
148
149
        //get all instances of the to be imported data object
150
        if ($this->deleteExistingRecords) {
151
            DataObject::get($this->objectClass)->removeAll();
152
        }
153
154
        return $this->processAll($filepath);
155
    }
156
157
    /**
158
     * Preview a file import (don't write anything to the database).
159
     * Useful to analyze the input and give the users a chance to influence
160
     * it through a UI.
161
     *
162
     * @param string $filepath Absolute path to the file we're importing
163
     * @return array See {@link self::processAll()}
164
     */
165
    abstract public function preview($filepath);
166
167
    /**
168
     * Process every record in the file
169
     *
170
     * @param string $filepath Absolute path to the file we're importing (with UTF8 content)
171
     * @param boolean $preview If true, we'll just output a summary of changes but not actually do anything
172
     * @return BulkLoader_Result A collection of objects which are either created, updated or deleted.
173
     * 'message': free-text string that can optionally provide some more information about what changes have
174
     */
175
    abstract protected function processAll($filepath, $preview = false);
176
177
178
    /**
179
     * Process a single record from the file.
180
     *
181
     * @param array $record An map of the data, keyed by the header field defined in {@link self::$columnMap}
182
     * @param array $columnMap
183
     * @param $result BulkLoader_Result (passed as reference)
184
     * @param boolean $preview
185
     */
186
    abstract protected function processRecord($record, $columnMap, &$result, $preview = false);
187
188
    /**
189
     * Return a FieldList containing all the options for this form; this
190
     * doesn't include the actual upload field itself
191
     */
192
    public function getOptionFields()
193
    {
194
    }
195
196
    /**
197
     * Return a human-readable name for this object.
198
     * It defaults to the class name can be overridden by setting the static variable $title
199
     *
200
     * @return string
201
     */
202
    public function Title()
203
    {
204
        $title = $this->config()->get('title');
205
        return $title ?: static::class;
206
    }
207
208
    /**
209
     * Get a specification of all available columns and relations on the used model.
210
     * Useful for generation of spec documents for technical end users.
211
     *
212
     * Return Format:
213
     * <code>
214
     * array(
215
     *   'fields' => array('myFieldName'=>'myDescription'),
216
     *   'relations' => array('myRelationName'=>'myDescription'),
217
     * )
218
     * </code>
219
     *
220
     * @todo Mix in custom column mappings
221
     *
222
     * @return array
223
     **/
224
    public function getImportSpec()
225
    {
226
        $singleton = DataObject::singleton($this->objectClass);
227
228
        // get database columns (fieldlabels include fieldname as a key)
229
        // using $$includerelations flag as false, so that it only contain $db fields
230
        $fields = (array)$singleton->fieldLabels(false);
231
232
        // Merge relations
233
        $relations = array_merge(
234
            $singleton->hasOne(),
235
            $singleton->hasMany(),
236
            $singleton->manyMany()
237
        );
238
239
        // Ensure description is string (e.g. many_many through)
240
        foreach ($relations as $name => $desc) {
241
            if (!is_string($desc)) {
242
                $relations[$name] = $name;
243
            }
244
        }
245
246
        return [
247
            'fields' => $fields,
248
            'relations' => $relations,
249
        ];
250
    }
251
252
    /**
253
     * Determines if a specific field is null.
254
     * Can be useful for unusual "empty" flags in the file,
255
     * e.g. a "(not set)" value.
256
     * The usual {@link DBField::isNull()} checks apply when writing the {@link DataObject},
257
     * so this is mainly a customization method.
258
     *
259
     * @param mixed $val
260
     * @param string $fieldName Name of the field as specified in the array-values for {@link self::$columnMap}.
261
     * @return boolean
262
     */
263
    protected function isNullValue($val, $fieldName = null)
0 ignored issues
show
The parameter $fieldName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

263
    protected function isNullValue($val, /** @scrutinizer ignore-unused */ $fieldName = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
264
    {
265
        return (empty($val) && $val !== '0');
266
    }
267
}
268