Search code examples
phpyii2e-commerce

Photo Online Store


I have to create a photo online store for my organization with some additional features. But the main idea is to allow users to upload their digital photos (JPEG format only) and sell them in the store. This project is much related to the websites like iStockPhotos, Fotolia etc...

Is there any standard I should follow for the JPEG images like minimum and maximum sizes and quality?

I am able to use libraries like such as Imagine to make watermark and thumbnail images from the original. But my main concern here, is how may I store the original and duplicate files safely in a proper folder structure securely and how do I create download links to the original files when someone purchased it?

I am planning to build this from scratch using PHP Yii2 framework. So please give me any guidelines to make it successful. Thanks in advance.


Solution

  • I'll split my answer in three parts. The upload-, saving- and the download-process. I expect you have experience with PHP and Yii2 so I won't code the whole thing out but give you the gist.

    1. Uploading photos

    This one is simple. Simply create a form where you can upload your image-files. Make sure you set the correct enctype (multipart/form-data). The basics of file-uploading with Yii2 are explained here.

    When saving your file you need to perform two steps:

    1. save the original
    2. create a resized and watermarked copy

    For the second task I can recommend you the library yurkinx/yii2-image which I made really good experience with. It has resizing and watermarking built in.

    2. Saving the photos

    I usually put them in a dedicated folder within @runtime. The runtime folder is within your project-root and therefore not accessible from the web. Your folder could have the following route, which you put in your params.php for later reference.

    @runtime/shop_images/
    

    For the actual file-names you could use the id of the model which I'll explain next. Your file-names could be:

    • 1.jpg (the original)
    • 1_thumb.jpg (the resized and watermarked copy)

    You should have a model for the photos. Each uploaded photo would be represented by a row in this models db-table. You also need a table to connect the users and their purchased images (m:n). Let me know if you need the exact schema.

    Watch out...you need to create the model-instance first and save it. Otherwise you won't have the id to name the actual files! The easiest way is to overwrite the save()-method od your photo-model as beforeSave() is too early (no id yet!) and afterSave() is too late (no more rollback possibility).

    3. Downloading the photos

    This one is way easier than it sounds. You create an action within your photo-conroller which lets the user download the pictures.

    In the action you first check if the user has purchased the picture. You can do this easily as you simply need to check if there is a row in your user_photo-table linking the currently logged in user to the requested photo.

    If the connection is there you are ready to output the photo. This involves two steps:

    1. Configure the response for outputting a photo
    2. The actual output

    Response configuration

    You can do this completely manual or use my extension. This is the manual way:

    public function actionPicture($id) {
        //find the model (will throw excetion if not)
        $model = $this->findModel($id);
    
        //check if user is allowed
        if (!UserPhoto::find()->user(Yii::$app->user->id)->photo($model)->exists()) {
            //throw not allowed exception here!
        }
    
        //get the path to your photo
        $photoPath= Yii::getAlias('@runtime/shop_images') . DIRECTORY_SEPARATOR . $model->id . '.jpg');
        $photoPath= FileHelper::normalizaPath($imagePath);
    
        //prepare the response
        $response = Yii::$app->response;
        $response->headers->set('Content-type', 'image/jpg');
        $response->headers->set('Content-length', filesize($photoPath));
        $response->format = \yii\web\Response::FORMAT_RAW;
    
        //return the contents
        return file_get_contents($photoPath);
    }
    

    To give you a little insight on this. Whatever you return from an action will be put in the data-attribute of your response. This will then be formatted into the content attribute of the response. How this will happen depends on the response format you set. In our case we chose FORMAT_RAW which leaves the data as is. Now we can simply return the contents of our photo-file and we are done.

    You can further specify how your file gets processed by specifying additional headers. This way for example you can force a download with a certain filename:

    $headers->set('Content-disposition', 'attachment; filename="' . $myCustomFileName . '"');
    

    The same way you create an action for your thumbs or extend this one with a second parameter which tells you what size of the image to return.

    I hope I could help. If you need any more information on one of the steps, don't hesitate to ask!

    DB-Schema

    Here is a possible migration to create the two tables needed. The user-table is omitted as you probably created this one already.

    public function up() {
        //the photo table and the relation to the owning user
        $this->createTable('{{%photo}}', [
            'id'=>$this->primaryKey(),
            'owner_user_id'=>$this->integer(),
            'original_filename'=>$this->string()->notNull(),
            'filesize'=>$this->integer()->notNull(),
            'extension'=>$this->string(4)->notNull(),
            'meta_data'=>$this->text(),
        ]);
        $this->addForeignKey('FK_photo_user', '{{%photo}}', 'owner_user_id', '{{%user}}', 'id', 'SET NULL', 'CASCADE');
    
        //the m:n-table mapping users to their purchased photos
        $this->createTable('{{%user_photo}}', [
            'user_id'=>$this->integer()->notNull(),
            'photo_id'=>$this->integer()->notNull(),
            'PRIMARY KEY (user_id, photo_id)',
        ]);
        $this->addForeignKey('FK_user_photo_user', '{{%user_photo}}', 'user_id', '{{%user}}', 'id', 'CASCADE', 'CASCADE');
        $this->addForeignKey('FK_user_photo_photo', '{{%user_photo}}', 'photo_id', '{{%photo}}', 'id', 'CASCADE', 'CASCADE');
    
        return true;
    }
    

    If you look at the schema you will get the gist.

    Photo-metadata (EXIF)

    If you wish to save meta-data for the photos, usually the EXIF-data, you can do this as well of course. I have to warn you though...there really is no real standard existing where the manufacturers of cameras store their values as I currently make the experience in one of our projects. In our example the most important information would have been the "date taken"-field. This alone can be in several places, depending the manufacturer. Making it even more complicated is the type of data within the EXIF. We had big trouble with illegal characters in there and about every charset-encoding you can imagine.

    Nonetheless I'll give you a snipped how we save the data in JSON-format in text column. The big advantage is that you have all the in its original structure. I also added the column to the migration above.

    $exif = exif_read_data($model->localFilePath, 'FILE,EXIF');
    if ($exif !== false) {
        $model->meta_Data = Json::encode($exif);
    }
    

    Make sure you read the documentation about exif_read_data() as it definitely has its kinks!