Import class using OOP
As I said in the comments:


  For errors, that's harder to say. Use exceptions for validation errors, catch them in the CSV reading loop, so you can continue reading. You'll have to decide what state to leave the Product class in if you do a partial read. If you don't mind passing the constructor an array of this data, even an optional array, you could do all the setting there $this->{"set$key"}($value) etc. But the big advantage is throwing an exception before the constructor completes destroys the object. Because the constructor can't return the instance in that case


Something like this 

class Product
{

    private $imageUrl = false;

    public function __construct(array $data = []){
          if(!empty($data)){
             foreach($data as $line=&gt;$value){
                if(isset($this-&gt;{$key})){
                  $this-&gt;{"set$key()"}($value);
                }
             }
         }
    }

    public setImageUrl($image){
          if(empty($image)) throw new InvalidArgumentException("Image is empty");
           $this-&gt;imageUrl= $image;

    }
}


There are a few tricks here, but you have to set the default to false for if(isset($this-&gt;{$key})) to work, but it's faster then property_exists.  This part will dynamically call setimageUrl in this case. The lowercase i doesn't matter much in setImageUrl as user defined methods/function are case insensitive.

The other part I said is this:


  This is my Opinion, 1. CSVImport , unless you also need to do arrays, in which case make another method for reading the data from the file, but it should be part of the class. The class won't work well without the CSV data. You'd have to make changes to both if fields are added etc.. You can also remove a loop by importing where the foreach is and using the file reading loop instead. 2. Product setters, if you ever need to manually create a product, you want them to do the validation too. 3. Should be part of validation, bad data goes in set, good data comes from get


Most of that is pretty self explanatory.  For #3 the -&gt;setImageUrl($this-&gt;websiteUrl.$imagePath); is part of the input data. So that is fine.  You don't want to have to pass them separately.  In fact with data loading in the constructor of the Product you can skip that method completely as it becomes:

 private function parseProductData($productData)
 {
     return new Product($productData);
 }


So that alone saves you a call to list and a call to parseProductData for each product.

I can give you more examples if you need them

I would do the CVSImport something like this (untested)

class CSVImport extends ProductImport
{
    private $websiteUrl;

    private $errors = [];

    public function __construct($websiteUrl)
    {
        $this-&gt;websiteUrl = $websiteUrl;
    }

    public function import($csvData)
    {
        $productCollection = new ProductCollection();
         //you could do file reading here with a while loop
        foreach ($csvData as $line=&gt;$rowData) {
            try{
                $productCollection-&gt;addProduct(new Product($rowData));
            }catch(Exception $e){
                $this-&gt;errors[$line] = get_class($e)."::{$e-&gt;getCode()} {$e-&gt;getMessage()} in {$e-&gt;getFile()} on {$e-&gt;getLine()}";
            } 

        }

        return $productCollection;
    }

    //or for max flexabillity you can do it here
    public function readCsv($filename){
        if(!file_exists($filename)) throw new Exception("Could not find file $filename".);//etc.

        $f = fopen($filename, 'r');

        $header = fgetcsv($f); //get header row
        $len = count($header);

        $line = 0;
        $rowData= [];
        while(!feof($f)){
            ++$line;

            try{
                $current = fgetcsv($f);

                if(count($current) != $len) throw new Exception("Delimter count missmatch on line $line of $filname");

                $rowData[$line] = array_combine($header, $current); //combine [$haders =&gt; $current]
            }catch(Exception $e){
                $this-&gt;errors[$line] = get_class($e)."::{$e-&gt;getCode()} {$e-&gt;getMessage()} in {$e-&gt;getFile()} on {$e-&gt;getLine()}";
            } 
        } 

        return $rowData;
    }

    public function getErrors(){
        return $this-&gt;errors;
    }

    public function hasErrors(){
         return count($this-&gt;errors)
    }

}


So you see the try/catch is almost the same, you could combine these two methods readCsv and import. But then you cannot accept array as data only the file. That's up to you.

The basics are that you can do new Product($data) and it does all the setting of the data using it's set methods.  Then if you throw exceptions in those set methods it not only helps you when using them manually, but when constructing a product from the CSV, the product dies as the exception is thrown. Then we catch that, keep track of it, and move on to the next row.  Each part handling it's own concerns.

In any case now it's all nice and clean, the other option would be to make a class just for reading CSV data, that takes headers as an argument, and does some mapping on them etc.  But its a lot easier to find then putting the CSV stuff in some random place.

Cheers.