Elph is a modular and easily customizable content management library that gives you pretty much everything you need for your basic phoenix project in terms of content-management.
It offers you easy content-management and a media library.
Just plug it into your existing phoenix app, add some configs, run migrations and you're ready to go.
NOTE: For the time being elph has no authentication//authorization integrated. This will be added in the future, probably as a plug-in module.
- Elph needs ffmpeg to be installed to automatically convert uploaded media-files to browser-friendlier formats. This includes creating thumbnails for images and videos as well as transcoding videos and audio to mp4/mp3.
- Add
{:elph, "~> 0.9.0}
to yourmix.exs
underdeps
- Run
mix deps.get
to fetch it. - Add the following to your
config.exs
config :elph,
repo: <YourApp>.Repo
upload_dir: "/app/uploads/"
url_upload_dir: "/uploads"
- Add the following to your
<YourAppWeb>Endpoint
- After your first
plug Plug.Static
-Block
- After your first
plug Elph.UploadPlug
-
- In your
plug Plug.Parsers
-Block append a length attribute. For examplelength: 100 * 1024 * 1024
if you want to have a max upload size of 100MB.
- In your
- Add the following supervisor to your
<YourApp>.Application
in thechilren
list in thestart/1
function
{Elph.MediaProcessing.BackgroundConverter, name: Elph.MediaProcessing.BackgroundConverter}
- Copy the migrations from
deps/elph/priv/repo/migrations
topriv/repo/migrations
and run them withmix ecto.create
To have the most control over everything you can use elph's contexts and controllers and need to do the routing yourself.
- Add a scope using
ElphWeb
as ControllerScope and add the Routes using the respective Contollers.- Example:
scope "/api", ElphWeb do pipe_through :api resources "/contents", ContentController, only: [:index, :show, :create, :delete] resources "/media", MediaController, only: [:create] end
- Example:
- Make sure to split those routes and guard them with an authorization mechanism as needed.
Elph has a default phoenix FallbackController to show errors and such. If you want to use your own customized FallbackController add the following to config.exs
: config :elph, fallback_controller: <YourAppWeb>.FallbackController
Elph brings with it a list of default types. Those can be found in elph/contents/types
. Those are included and available by default without more configuration.
In case this is not enough, you'll need to setup the use of custom types once and adding new types after that is pretty easy.
First we need to create the central point where we define our custom types.
- Create a new module like so
defmodule <YourApp>.Contents.Types do
use Elph.Contents.Types
elph_types()
end
- Add this module to the
config.exs
config :elph, types: <YourApp>.Contents.Types
In this file you'll later add your types. In case you don't want to add all elph_types()
you can use only: [:html, :audio]
or except: [:html, :audio]
to refine your choice.
Start off with a new schema created by mix phx.gen.schema
.
For example: mix phx.gen.schema Contents.Types.Markdown markdown_contents markdown:text
- Hint: As a convention all Elph content types use
<type>_contents
as their schema source.
Now we need to change some stuff in the <YourApp>.Contents.Types.Markdown
module.
- Add
use Ecto.Contents.ContentType
belowuse Ecto.Schema
. - Change
schema "markdown_contents" do
tocontent_schema "markdown_contents" do
. - Remove the
timestamps()
call in your schema as Elph already manages timestamps. - Write your
changeset
as you would usually and only care about your own stuff. Don't embed or cast content-variables or child-contents. This will be handled automatically.- Care! The function has to be called
changeset
so it can be called by Elph
- Care! The function has to be called
- If you need associated data to be preloaded automatically you can add
preloads(action)
to your module. This will be passed toEcto.Repo.preload
after your contents have been fetched from the database. Beware: If you have associated elph contents, you don't get whole contents. You'll only get the content type specific data, not the general information (type
,name
andshared
) or the contents' children. This means you can't use the default elphrender
for those contents. If you need all the fields or even children usecontent_preloads
(see below). Usingpreloads
is faster and you should use it even for contents, if you don't need all fields. - If you need all contents fields or even a whole subtree use
content_preloads(action)
. Beware: This works a little different thenEcto.Repo.preload
.- You can only preload
:atom
or[:atom1, :atom2]
. The otherpreload
syntaxes are not supported. Nested preloads aren't either. - While loading the contents their
preloads
andcontent_preloads
functions will also be loaded. Take care not to create cycles! (In case you doelph
will have a fallback limiting the recursion depth to3
so you don't end in an infinity loop. This can be changed via config optioncontent_preloads_max_depth
if your data structs are nested deeper then this) - As with
Ecto.Repo.preload
this will also not preload associations that are already loaded. So make sure not to include your association in bothpreloads(_)
andcontent_preloads(_)
.
- You can only preload
Add your new type in your central content definition module (See paragraph above).
type(:markdown, <YourApp>.<YourContext>.Markdown, <YourAppWeb>.MarkdownView)
with the following params- The
name
of your type - The newly created module
<YourApp>.<YourContext>.MarkdownContent
- The view which will be called to render the result.
- It needs a
def render("markdown.json", %{content: content}) do
function.- You can use
%{content: content, action: action}
instead, if you need to do rendering depending on this information. - Defaults are
:index
and:show
.
- You can use
- Care! Use
<name>.json
as first param.
- It needs a
- The
We also need to alter the created migration.
- Add
use Elph.Migration
belowuse Ecto.Migration
- In the
create table
call addadd_content_field()
and remove thetimestamps()
.
If you need your custom types to be cleaned up after them, you can add one or more callbacks.
Add a def after_delete_callback(content) do
to your custom content type module. This callback will be called
once for each (explicitly or via garbage collection) deleted content with the deleted content as parameter.
If you need the cleanup to re-run afterwards return :cleanup
. All other returns are ignored.
As with custom types you'll first need to create a module:
defmodule <YourApp>.Callbacks do
use Elph.Contents.Callbacks
elph_cleanup_callbacks()
end
Now you can add one or more callbacks; for example cleanup_callback(&IO.inspect/1)
Each callback will be called with a list of explicitly deleted and garbage-colledted content (without its children, as one would get from Contents.list_contents
). So the above example would print a list of the deleted contents on your console.
If you want to rerun the cleanup after all callbacks were run, return :cleanup
in your function. Every other return will be ignored. Care not to produce infinity loops!
For the development of elph we created a project called Elph-Shell. It provides an api with some basic functionality. It also has some configuration set to make developing elph a little easier.
To test elph you'll need a database. Per default Elph uses a mysql Database and reads the DATABASE_URL
environment variable to aquire credentials.
In case you don't want to use MySql or a DATABASE_URL
you can change your config.ex
file. Additionally you'll need to change the adapter in Elph.Test.Repo
and import your dependancy via mix
.
To run tests from within elph-shell you need to create a new database for testing, since using the development database will give you errors. For that open a shell to the docker-container
docker-compose enter phoenix bash
Login to mysql as rootmysql -hdb -uroot -pmysql
Create a database and givemysql
all permissionsCREATE DATABASE elph_test;
GRANT ALL PRIVILEGES ON elph_test.* TO 'mysql'@'%';
To run your test simply change to the elph folder in case you're in the shell-directory with cd elph
. For the first time - and after changing your migrations - you'll have to run DATABASE_URL=mysql://mysql:mysql@db/elph_test MIX_ENV=test mix ecto.reset
.
After that you can run DATABASE_URL=mysql://mysql:mysql@db/elph_test mix test
and everything should work out. If you want to see test-coverage you can add the parameter --cover
to the command and elixir will show you a coverage percentage and put detailed reports into the cover-subdirectory.