The MediaBundle provides similar features that the nice
SonataMediaBundle, but without the tight coupled
SonataAdminBundle parts and with some different OOP design approaches., (*1)
Important Note, (*2)
Once again, i'm not inventing here, the bundle is heavily inspired from the great
SonataMediaBundle and
@rande is the brain behind the context/provider behaviour ;-), (*3)
The AnoMediaBundle is just different in the way it handles the models, the contexts/providers and the events behind that.
That's also why the README file is essentially about the models :p, (*4)
However, i didn't chose to simply fork the SonataMediaBundle,
but to rewrite the code from scratch. That's the way i chose to understand all the DIC advanced configuration,
and to forge a bundle which fits at best my personal needs. But keep in mind that a lot of code from SonataMediaBundle
is reused at some places., (*5)
The MediaBundle provides a generic and universal way to handle different kinds of medias in a project.
It provides a bunch of services, each responsible for a specific task in the media journey., (*6)
It tries to abstract the filesystem through the nice and funny KnpLabs Gaufrette library, but also provides an abstraction
of CDN use., (*7)
The understanding of the bundle behaviour requires some bases knowledge in OOP design.
The understanding of the code itself, especially the configuration part requires strong skills with Symfony 2
and Dependency Injection system/concept., (*8)
Installation
Dependencies
AnoSystemBundle
Most of the Ano bundles have a dependency on a AnoSystemBundle
which provides common behaviour such as DIC parameters remapping in extensions, or an inflector, or even an AclManager., (*9)
If you don't want the dependencies you can just c/c the remapParametersNamespaces
and remapParameters
from the
AnoSystemBundle Extension to the MediaExtension file and get rid of the AnoSystemBundle., (*10)
Gaufrette
The bundle has a dependency with the KnpLabs Gaufrette library, so you need to
install it in your projects and make sure to configure the autoloader for it., (*11)
Imagine
If you intend to use the default ImageManipulator implementation, you need to install the great
Imagine library from Bulat Shakirzyanov., (*12)
The bundle goes to the vendor/bundles/Ano/Bundle
directory (so you should have vendor/bundles/Ano/Bundle/MediaBundle
), (*13)
Concept
Context/Provider concepts
The MediaBundle concept key is about context
.
What is a context
?
A context
is used to identify an use case for a Media in the application. It's needed by the MediaBundle to be able
to use the appropriate media provider for a specific media type (a video or an image for instance) and to know what
to do with it., (*14)
A provider
is an object responsible for retrieving a specific media type, like a video for instance, and to perform the
operations relative to this media type. For example, an ImageProvider will know how to retrieve an image from filesystem and
how to generate thumbs (trought an image manipulator interface), while a VideoProvider will know how to retrieve metadata
from a WS API like Dailymotion or Youtube ones., (*15)
So the provider
is determined from the context
, and some configuration are given to it depending on the user needs
(several thumb sizes to be generated for instance). A bunch of tools are given to the Provider
depending on the media type
to be handled, like Gaufrette drivers for accessing the filesystem, or an Image manipulator implementation for
generating thumbs, etc, etc., (*16)
Implementation
So, enough about talking, let's take an example., (*17)
Scenario
Say an user in our application can add an image as an avatar, and can eventually remove it.
The application is implemented under Symfony 2 and the user can interact through a standard Form., (*18)
Image files are stored in a medias
folder inside the project structure, in the local filesystem, but are accessible
through a different host., (*19)
For display matters, the system needs two specific thumb sizes, small: 50x50
and medium: 90x90
in addition of the original file., (*20)
Configuration
First, let's add some configuration according to our needs. We need to add the following in the config.yml
file :, (*21)
ano_media:
cdn:
local:
default: true
id: ano_media.cdn.remote_server
options:
base_url: "http://img.my.local" #(ideally, this parameter should lives in the parameters.yml file)
provider:
image: # So we need the image provider here
default: true
id: ano_media.provider.image
filesystem:
local:
default: true
id: ano_media.filesystem.local
options:
base_path: %kernel.root_dir%/../medias
create: true #creates the directory it it doesn't exist yet
contexts:
user_picture:
formats:
small: { width: 50, height: 50 }
medium: { width: 90, height: 90 }
That's it for the configuration, that's all we need., (*22)
The Model
Now, we need to define our models to meet our needs, but before we do so, let's do some theory., (*23)
In a single application, there is often the need for different kinds of media objects : an avatar for the user, an image for
an article or a news item, etc etc., (*24)
In the OOP world, this should be represented by "independent" class definitions, all sharing a common behaviour.
But as you certainly are wondering, how do we persist this ?, (*25)
The response is: it doesn't matter, with the power of nowadays ORM, you should not be concerned about that.
Just create your models, and you'll take care of that later., (*26)
So, here we go :, (*27)
First, we need a base Media class, which in most cases will not be used directly., (*28)
namespace My\SiteBundle\Model;
use Ano\Bundle\MediaBundle\Model\Media as BaseMedia;
abstract class Media extends BaseMedia
{
}
Now, we need an object for each Media "use case". From an OOP point of view, an user picture (avatar) and other images for
any other purposes should be represented by their own class, since their purposes in the system are different., (*29)
So, for our user picture we need a specific Media :, (*30)
namespace My\UserBundle\Model;
use My\SiteBundle\Model\Media as BaseMedia;
class UserPicture extends BaseMedia
{
protected $context = 'user_picture';
}
Naturally, we need to define the relation with our User object (unidirectional here) :, (*31)
namespace My\UserBundle\Model;
class User
{
private $name;
private $picture;
public function setPicture(UserPicture $picture)
{
$this->picture = $picture;
}
public function getPicture()
{
return $this->picture;
}
}
That's it for our model, nice and simple OOP., (*32)
ORM Configuration
Now we defined our model, we need to create metadata for the ORM.
Here we'll use Doctrine 2 and standard relational database., (*33)
Like we said before, we could have several kinds of medias in a single application. But since my different media objects
don't define specific data, we could persist them in a single shared database table. We'll use Doctrine SINGLE-TABLE INHERITANCE
behaviour for this., (*34)
First of all, we need to add an identity to our Media base model, that's how we transform it into an entity (in the Doctrine world).
To be concise, i'll add the identifier directly inside the Media model class (but we could also create another namespace named Entity
and create media subclasses here in order to isolate the Doctrine specifics)., (*35)
namespace My\SiteBundle\Model;
use Ano\Bundle\MediaBundle\Model\Media as BaseMedia;
abstract class Media extends BaseMedia
{
protected $id;
public function setId($id)
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
}
And now, our mapping :, (*36)
My/SiteBundle/Resources/config/doctrine/Media.orm.xml
:, (*37)
<entity name="My\SiteBundle\Model\Media" table="medias" inheritance-type="SINGLE_TABLE">
<id name="id" type="integer" column="id">
<generator strategy="AUTO" />
</id>
<discriminator-column name="discr" type="string" />
<discriminator-map>
<discriminator-mapping value="media" class="Media" />
<discriminator-mapping value="user_picture" class="My\UserBundle\Model\UserPicture" />
</discriminator-map>
</entity>
My/UserBundle/Resources/config/doctrine/UserPicture.orm.xml
:, (*38)
<entity name="My\UserBundle\Model\UserPicture" />
My/UserBundle/Resources/config/doctrine/User.orm.xml
:, (*39)
<entity name="My\UserBundle\Model\User" table="users">
<id name="id" type="integer" column="id">
<generator strategy="AUTO" />
</id>
<field name="name" column="name" type="string" length="30" />
<one-to-one field="picture" target-entity="My\UserBundle\Model\UserPicture">
<cascade>
<cascade-all />
</cascade>
</one-to-one>
</entity>
As you can see, i use the Doctrine operation cascading. That means that when you'll perform a persist operation
on an User instance ($em->persist($user)), Doctrine will cascade this operation to the UserPicture object living
inside the User., (*40)
That is useful to avoid having to persist manually the UserPicture, which is not really logical.
The UserPicture is not living outside of an User instance, so from an OOP point of view, when we alter the user picture,
we alter the user, and then, the user data needs to be saved (thus the user picture will be)., (*41)
Naturally, that's exactly the same case on a remove operation., (*42)
Frontend operation
The user needs to interact with the system in order to provide the picture he wants as his avatar.
We'll just use a simple Symfony Form for that. To be simple, i'll use a simple array as data for the form., (*43)
The idea is that we need the Form component to map the user uploaded file to a File object so we can easily get the binary
content of the file and set it into our UserPicture object., (*44)
My/UserBundle/Form/UserProfileType
:, (*45)
namespace My\UserBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilder;
class UserProfileType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('picture', 'file')
;
}
public function getName()
{
return 'user_profile';
}
}
Controller
:, (*46)
public function editPictureAction(Request $request)
{
$data = array('picture');
$form = $this->formFactory->create(new UserProfileType(), $data);
if ('POST' == $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$user = $this->getCurrentUser();
if (!empty($data['picture'])) {
$userPicture = new UserPicture();
$userPicture->setName($data['picture']->getClientOriginalName());
$userPicture->setContent($data['picture']);
$user->setPicture($userPicture);
$this->userManager->saveUser($user);
$this->session->setFlash('notice', 'Avatar saved !');
}
return $this->getResponseRedirect('my_user_profile_edit');
}
else {
$this->session->setFlash('errors', 'Validation errors, please fix your inputs');
}
}
return $this->render('MyUserBundle:User:edit-profile', array(
'form' => $form->createView(),
));
}
And that's it, the MediaBundle will magically perform all the needed operations for an UserPicture.
If you take a look at your medias directory, you should see all the generated thumbs, and if you look at your database
you'll see the media is correctly persisted :-), (*47)