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.exsunderdeps - Run
mix deps.getto 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 * 1024if you want to have a max upload size of 100MB.
- In your
- Add the following supervisor to your
<YourApp>.Applicationin thechilrenlist in thestart/1function
{Elph.MediaProcessing.BackgroundConverter, name: Elph.MediaProcessing.BackgroundConverter}
- Copy the migrations from
deps/elph/priv/repo/migrationstopriv/repo/migrationsand 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
ElphWebas 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>_contentsas their schema source.
Now we need to change some stuff in the <YourApp>.Contents.Types.Markdown module.
- Add
use Ecto.Contents.ContentTypebelowuse Ecto.Schema. - Change
schema "markdown_contents" dotocontent_schema "markdown_contents" do. - Remove the
timestamps()call in your schema as Elph already manages timestamps. - Write your
changesetas 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
changesetso 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.preloadafter 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,nameandshared) or the contents' children. This means you can't use the default elphrenderfor those contents. If you need all the fields or even children usecontent_preloads(see below). Usingpreloadsis 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
:atomor[:atom1, :atom2]. The otherpreloadsyntaxes are not supported. Nested preloads aren't either. - While loading the contents their
preloadsandcontent_preloadsfunctions will also be loaded. Take care not to create cycles! (In case you doelphwill have a fallback limiting the recursion depth to3so you don't end in an infinity loop. This can be changed via config optioncontent_preloads_max_depthif your data structs are nested deeper then this) - As with
Ecto.Repo.preloadthis 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
nameof 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}) dofunction.- You can use
%{content: content, action: action}instead, if you need to do rendering depending on this information. - Defaults are
:indexand:show.
- You can use
- Care! Use
<name>.jsonas first param.
- It needs a
- The
We also need to alter the created migration.
- Add
use Elph.Migrationbelowuse Ecto.Migration - In the
create tablecall 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 bashLogin to mysql as rootmysql -hdb -uroot -pmysqlCreate a database and givemysqlall 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.