If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeCheck out the homepage: every Article
has an author. But, open the Article
entity. Oh: the author
property is just a string!
... lines 1 - 15 | |
class Article | |
{ | |
... lines 18 - 47 | |
/** | |
* @ORM\Column(type="string", length=255) | |
*/ | |
private $author; | |
... lines 52 - 247 | |
} |
When we originally created this field, we hadn't learned how to handle database relationships yet.
But now that we are way more awesome than "past us", let's replace this author
string property with a proper relation to the User
entity. So every Article
will be "authored" by a specific User
.
Wait... why are we talking about database relationship in the security tutorial? Am I wandering off-topic again? Well, only a little. Setting up database relations is always good practice. But, I have a real, dubious, security-related goal: this setup will lead us to some really interesting access control problems - like denying access to edit an Article
unless the logged in user is that Article's author.
Let's smash this relationship stuff so we can get to that goodness! First, remove the author
property entirely. Find the getter and setter methods and remove those too. Now, find your terminal and run:
php bin/console make:migration
If our app were already deployed, we might need to be a little bit more careful so that we don't lose all this original author data. But, for us, no worries: that author data was garbage! Find the Migrations/
directory, open up the new migration file and yep! ALTER TABLE Article DROP author
:
... lines 1 - 2 | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20180901184240 extends AbstractMigration | |
{ | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article DROP author'); | |
} | |
public function down(Schema $schema) : void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article ADD author VARCHAR(255) NOT NULL COLLATE utf8mb4_unicode_ci'); | |
} | |
} |
Now, lets re-add author as a relation:
php bin/console make:entity
Update the Article
entity and add a new author
property. This will be a "relation" to the User
entity. For the type, it's another ManyToOne
relation: each Article
has one User
and each User
can have many articles. The author
property will be required, so make it not nullable. We'll say "yes" to mapping the other side of the relationship and I'll say "no" to orphanRemoval
, though, that's not important. Cool! Hit enter to finish:
... lines 1 - 15 | |
class Article | |
{ | |
... lines 18 - 68 | |
/** | |
* @ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="articles") | |
* @ORM\JoinColumn(nullable=false) | |
*/ | |
private $author; | |
... lines 74 - 237 | |
public function getAuthor(): ?User | |
{ | |
return $this->author; | |
} | |
public function setAuthor(?User $author): self | |
{ | |
$this->author = $author; | |
return $this; | |
} | |
} |
Now run:
php bin/console make:migration
Like always, let's go check out the new migration:
... lines 1 - 2 | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20180901184346 extends AbstractMigration | |
{ | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article ADD author_id INT NOT NULL'); | |
$this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); | |
$this->addSql('CREATE INDEX IDX_23A0E66F675F31B ON article (author_id)'); | |
} | |
public function down(Schema $schema) : void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E66F675F31B'); | |
$this->addSql('DROP INDEX IDX_23A0E66F675F31B ON article'); | |
$this->addSql('ALTER TABLE article DROP author_id'); | |
} | |
} |
Woh! I made a mistake! It is adding author_id
but it is also dropping author
. But that column should already be gone by now! My bad! After generating the first migration, I forgot to run it! This diff contains too many changes. Delete it. Then, execute the first migration:
php bin/console doctrine:migrations:migrate
Bye bye original author
column. Now run:
php bin/console make:migration
Go check it out:
... lines 1 - 2 | |
namespace DoctrineMigrations; | |
use Doctrine\DBAL\Schema\Schema; | |
use Doctrine\Migrations\AbstractMigration; | |
/** | |
* Auto-generated Migration: Please modify to your needs! | |
*/ | |
final class Version20180901184346 extends AbstractMigration | |
{ | |
public function up(Schema $schema) : void | |
{ | |
// this up() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article ADD author_id INT NOT NULL'); | |
$this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)'); | |
$this->addSql('CREATE INDEX IDX_23A0E66F675F31B ON article (author_id)'); | |
} | |
public function down(Schema $schema) : void | |
{ | |
// this down() migration is auto-generated, please modify it to your needs | |
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.'); | |
$this->addSql('ALTER TABLE article DROP FOREIGN KEY FK_23A0E66F675F31B'); | |
$this->addSql('DROP INDEX IDX_23A0E66F675F31B ON article'); | |
$this->addSql('ALTER TABLE article DROP author_id'); | |
} | |
} |
Much better: it adds the author_id
column and foreign key constraint. Close that and, once again, run:
php bin/console doctrine:migrations:migrate
Woh! It explodes! Bad luck! This is one of those tricky migrations. We made the new column required... but that field will be empty for all the existing rows in the table. That's not a problem on its own... but it does cause a problem when the migration tries to add the foreign key! The fix depends on your situation. If our app were already deployed to production, we would need to follow a 3-step process. First, make the property nullable=true
at first and generate that migration. Second, run a script or query that can somehow set the author_id
for all the existing articles. And finally, change the property to nullable=false
and generate one last migration.
But because our app has not been deployed yet... we can cheat. First, drop all of the tables in the database with:
php bin/console doctrine:schema:drop --full-database --force
Then, re-run all the migrations to make sure they're working:
php bin/console doctrine:migrations:migrate
Awesome! Because the article
table is empty, no errors.
Now that the database is ready, open ArticleFixtures
. Ok: this simple setAuthor()
call will not work anymore:
... lines 1 - 10 | |
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface | |
{ | |
... lines 13 - 24 | |
private static $articleAuthors = [ | |
'Mike Ferengi', | |
'Amy Oort', | |
]; | |
... line 29 | |
protected function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(10, 'main_articles', function($count) use ($manager) { | |
... lines 33 - 59 | |
$article->setAuthor($this->faker->randomElement(self::$articleAuthors)) | |
... lines 61 - 62 | |
; | |
... lines 64 - 70 | |
}); | |
... lines 72 - 73 | |
} | |
... lines 75 - 81 | |
} |
Nope, we need to relate this to one of the users from UserFixture
. Remember we have two groups: these main_users
and these admin_users
:
... lines 1 - 9 | |
class UserFixture extends BaseFixture | |
{ | |
... lines 12 - 18 | |
protected function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(10, 'main_users', function($i) use ($manager) { | |
... lines 22 - 40 | |
}); | |
$this->createMany(3, 'admin_users', function($i) { | |
... lines 44 - 54 | |
}); | |
... lines 56 - 57 | |
} | |
} |
Let's allow normal users to be the author of an Article
. In other words, use $this->getRandomReference('main_users')
to get a random User
object from that group:
... lines 1 - 10 | |
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface | |
{ | |
... lines 13 - 24 | |
protected function loadData(ObjectManager $manager) | |
{ | |
$this->createMany(10, 'main_articles', function($count) use ($manager) { | |
... lines 28 - 54 | |
$article->setAuthor($this->getRandomReference('main_users')) | |
... lines 56 - 57 | |
; | |
... lines 59 - 65 | |
}); | |
... lines 67 - 68 | |
} | |
... lines 70 - 77 | |
} |
At the top of the class, I can remove this old static property.
Try it! Move over and run:
php bin/console doctrine:fixtures:load
It works! But... only by chance. UserFixture
was executed before ArticleFixtures
... and that's important! It would not work the other way around. We just got lucky. To enforce this ordering, at the bottom of ArticleFixtures
, in getDependencies()
, add UserFixture::class
:
... lines 1 - 10 | |
class ArticleFixtures extends BaseFixture implements DependentFixtureInterface | |
{ | |
... lines 13 - 70 | |
public function getDependencies() | |
{ | |
return [ | |
... line 74 | |
UserFixture::class, | |
]; | |
} | |
} |
Now UserFixture
will definitely run before ArticleFixtures
.
If you try the fixtures again:
php bin/console doctrine:fixtures:load
Same result. But now, it's guaranteed!
Next - let's finish our refactoring and create a new "Article Edit" page!
Hey @Matt
Thanks for the feedback, we don't love to say "start over from our code" but sometimes it's unavoidable
Cheers!
Reference to: (App\Entity\Users_0) already exists, use method setReference in order to override it
Any ideea ?
Hey Ad F.!
Hmm. I might have an idea. Did you download the fresh "start" code for this tutorial? Or are you using the code from the previous tutorials? I'm asking because, before this tutorial, I did some refactoring on our fixture system to make it more robust. Based on your error, it *looks* like you may be using the old code - but let me know.
Cheers!
i’m using the code from previous tutorials.
also. you are using comment entity on ArticlesFixtures but you don’t mention it.
Hey @cybernet2u!
Ah! That’s it then! At the very beginning of the tutorial, I mention that some of the fixture code has been updated between the tutorials and that you should download thecfresh code to get it. The way I built the createMany() function in the fixtures was a bit limited. I fixed that before this tutorial.
I hope that help!
Cheers!
i got the fresh code, now i'm trying to work with relations
$category->setParentCategory($this->getRandomReferences(MainCategory::class, $this->faker->numberBetween(0, 9)));
Argument 1 passed to App\Entity\Category::setParentCategory() must be an instance of App\Entity\MainCategory or null, array given.
don't know how to fix it ...
no fixture seems to persist ...
Hi Ad F.
->setParentCategory() expects one object and you are using $this->getRandomReferences() which returns array of objects, you should use $this->getRandomReference to get only one object.
Hope this will fix your error
Cheers!
Hey Remus M.!
Interesting! I wonder if something is being called recursively... or if something is stuck in a loop! Do you have XDebug installed? It would at least throw an error in case there is something being called recursively. Or, you can post your code and we can have a look. It definitely looks strange!
Cheers!
can you please update the tutorial, what you have in course script is different from the code on the website, it's very confusing :(
$this->createMany(10, 'main_articles', function($count) use ($manager)
is different than the one in BaseFixture
Hey Remus M.!
Hmm. What do you mean by the code in the "course script"? Do you mean the code blocks on the page? Or do you mean the code download? Or something different? Those are actually all generated from the same exact place. But I do think there is something that, at the very least, must be confusing. Let me know what you're seeing and we'll see what we can do! :)
Cheers!
if i use the code from website code blocks
" $this->createMany(10, 'main_articles', function($count) use ($manager) "
it's not "compatible" with functions from BaseFixture
" protected function createMany(string $className, int $count, callable $factory) "
Are you using the code from the previous tutorial? Because Ryan made some improvements to the fixtures base class on this tutorial
Really? That's weird, I just downloaded it and double checked the method's signature. It looks fine:
// src/DataFixtures/BaseFixture.php
...
protected function createMany(int $count, string $groupName, callable $factory)
Ohh right! that's because of the improvements that Ryan made to that class for this Course. The base class from the previous course is not compatible for this course.
I'm sorry for all this confussion
figured as much, however the problem is still there
how do i accomplish what i need, setAuthor, from a generated Fixture ?
Hey Ad F.!
I've just pushed a pull request to your BitBucket repository that updates all of your fixture classes to the latest version and fixes an infinite recursion issue that accidentally slipped into your code. Each change is on its own commit, so I hope it will help you forward!
Cheers!
Hi Ryan,
when trying to load the fixtures I get this:
`
php bin/console doctrine:fixtures:load
Careful, database will be purged. Do you want to continue y/N ?y
> purging database
> loading App\DataFixtures\TagFixture
> loading App\DataFixtures\UserFixture
> loading App\DataFixtures\ArticleFixtures
In BaseFixture.php line 74:
Did not find any references saved with the group name "main_users"
doctrine:fixtures:load [--append] [--em EM] [--shard SHARD] [--purge-with-truncate] [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no-interaction] [-e|--env ENV] [--no-debug] [--] <command>
`
I am afraid I don't find the error 8-(.
Thx for any assistance
Hey Oliver,
Look closer to your fixtures code, looks like Fixtures system cannot find any reference of "main_users". I suppose you need to look at your UserFixture class where we create those references with "main_users" name. Probably you mistyped it, e.g. call it "main_user"?
I hope this helps!
Cheers!
Hi Ryan,
I dont know is it write place for this problem but on
https://stackoverflow.com/q...
there is no solution.
Here is the problem
Picture for user is not directly in user entity (it is many to one with gallery)
Also I have to mention that on submit, action (edit action) is completed and I am able to change picture, but I got this erorr
Serialization of 'Symfony\Component\HttpFoundation\File\File' is not allowed
before is redirected to route
Any help?
Thanks anyway
Hey sasa1007
I think the easiest thing you can do is to NOT serialize the File
property of your User class
Cheers!
Thank you, but I did not serialize anything.
I saw on the stackoverflow that problem is:
"The problem is, when User Entity was implementing the UserInterface, the user provider(actually the Doctrine, behind the scene) tried to Serializing the User object to store it in the session but because of the file that I assigned it to this class, it fails it's career!"
I dont know how to control what will be Serialized what not.
This is my Entity:
class Team
{
...
/**
* @ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="team")
*/
private $users;
public function __construct()
{
$this->users = new ArrayCollection();
}
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(User $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
$user->setTeam($this);
}
return $this;
}
public function removeUser(User $user): self
{
if ($this->users->contains($user)) {
$this->users->removeElement($user);
// set the owning side to null (unless already changed)
if ($user->getTeam() === $this) {
$user->setTeam(null);
}
}
return $this;
}
...
}
Hey sasa1007!
Ah, yes. I think I understand what's going on. At the end of the request, the User object that the user is logged in as is serialized to the session. By default, all properties are serialized. Normally... that's great! But you've found a case where it *doesn't* work great: we want this "picture" property (or whatever it's called) to *not* be serialized. There are 2 solutions:
1) (the solution I like less) Make your User class implement SerializableInterface and then take control of which fields are serialized and deserialized. Be sure to include the id, password, roles and whatever your getUsername() method returns (so, the username or email property). I hate doing this... because it's annoying and error prone.
2) After you handle the upload inside your "edit" controller, just set that property back to null - e.g. $user->setPicture(null). Then, when the User object is serialized to the session at the end of the request, it should work fine.
Let me know how it goes!
Cheers!
Actually, the first solution is more atractive to me, to take control over what I want to be serialized, but I dont know how, so I try second solution and working fine.
Thank You so much
Hello
I have the following issue with migrations in general:
From the second or third migration until today it adds repeatedly more and more unnecessary additional queries to the migration.
example: it adds to every migration: ALTER TABLE user CHANGE roles roles JSON NOT NULL, CHANGE twitter_username twitter_username VARCHAR(255) DEFAULT NULL
even if the migration only deals about: lets say: article
I mean its not a real "problem" to have more in the migration and to ignore the additional queries.
But it would be cool to have it as clean as possible and nice looking like in the tutorial :-)
The last migration from the tutorial folder looks like this:<br />$this->addSql('ALTER TABLE article ADD author_id INT NOT NULL');<br />$this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)');<br />$this->addSql('CREATE INDEX IDX_23A0E66F675F31B ON article (author_id)');<br />
My last one looks like this:<br />$this->addSql('ALTER TABLE article ADD author_id INT NOT NULL, CHANGE published_at published_at DATETIME DEFAULT NULL, CHANGE image_filename image_filename VARCHAR(255) DEFAULT NULL');<br />$this->addSql('ALTER TABLE article ADD CONSTRAINT FK_23A0E66F675F31B FOREIGN KEY (author_id) REFERENCES user (id)');<br />$this->addSql('CREATE INDEX IDX_23A0E66F675F31B ON article (author_id)');<br />$this->addSql('ALTER TABLE post CHANGE category_id category_id INT DEFAULT NULL, CHANGE image image VARCHAR(255) DEFAULT NULL');<br />$this->addSql('ALTER TABLE user CHANGE roles roles JSON NOT NULL, CHANGE twitter_username twitter_username VARCHAR(255) DEFAULT NULL');<br />
I thought, maybe there is something in the config files, but didnt find anything what could force such beahviour.
I use symfony 4.3
My config files:
doctrine_migration.yaml:
`
doctrine_migrations:
dir_name: '%kernel.project_dir%/src/Migrations'
# namespace is arbitrary but should be different from App\Migrations
# as migrations classes should NOT be autoloaded
namespace: DoctrineMigrations
`
doctrine.yaml:
`
doctrine_migrations:
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_mysql'
server_version: '5.7'
charset: utf8mb4
default_table_options:
charset: utf8mb4
collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
orm:
auto_generate_proxy_classes: true
naming_strategy: doctrine.orm.naming_strategy.underscore
auto_mapping: true
mappings:
App:
is_bundle: false
type: annotation
dir: '%kernel.project_dir%/src/Entity'
prefix: 'App\Entity'
alias: App
`
Thanks very much for help
Hey Christoph
That's odd. Could you double check that your database is in sync with your mapping files? You can run php bin/console d:s:validate
Other things to consider:
cheers!
Hi Diego and thanks for your help
Yes, you are right: the database is not in sync.I ran doctrine:schema:validate
and it says:
Mapping
[OK] The mapping files are correct.
Database
[ERROR] The database schema is not in sync with the current mapping file
I am doing cache:clear very frequently.
I double-checked that im on dev.
I made a recreate of database:
<br />doctrine:schema:drop --force --full-database<br />Deleted all migrations-files in src/Migrations<br />make:migration (created 1 bigger migration file)<br />doctrine:migrations:migrate<br />
But i still got the same error on: doctrine:schema:validate . My database is still not in synch.
Maybe its my environment, im doing on Windows and XAMPP php 7.1 which comes with MariaDB instead of MySQL.
MariaDB and MySQL is nearly identical, but maybe in this case: only "nearly".... :-)
So, i will give up on this for now and will not dig in too deep. Maybe at a later moment...
greetings
Chris
Hey Chris!
Ah, indeed - the MariaDB part might hold the key! Check out this issue: https://github.com/doctrine/dbal/issues/2985 - and the possible fix - https://github.com/symfony/symfony-docs/pull/9547/files - that server_version
is config that you'll find in your config/packages/doctrine.yaml
file.
Let me know if it helps!
Cheers!
Am I right?./bin/console doctrine:database:drop --force #This drops only the Content of the DB, you showed this command in another tutorial "what to do if migrations go wrong"<br />./bin/console doctrine:schema:drop --full-databse --force #Drops the complete database with column names etc
So I could always execute the second query only, because it drops everything, am I right?
Hey Mike,
Nope, you're confused with those commands. If you need to drop DB schema, i.e. remove tables in the DB, use:
bin/console doctrine:schema:drop --force
But the command will drop only tables that are pointed from your entities, i.e. will use Class Metadata to detect the database table schema. If you want to drop ALL tables even if they do not have corresponding entity in your project, use:
bin/console doctrine:schema:drop --force --full-database
If you want to drop the DB, i.e. including all the tables inside of it, use:
bin/console doctrine:database:drop --force
So, if you want to drop DB - you may skip the doctrine:schema:drop command because the last one will remove the DB that means all its tables will be removed as well.
P.S. If you not sure about the command - use --help option to get more context about those commands, i.e.
bin/console doctrine:schema:drop --help
Cheers!
Hi
At 4'43 you say "run a script or query that can somehow set the article_id", but I think you meant "set the author_id".
Hey Nethik
Yep, that's totally a Ryan's mistake :)
Thanks for highlighting it, we will fix it soon. Cheers!
Hey Dominik
I believe that part is never shown on the videos, it is just an upgrade Ryan made to the Fixtures base class, so now you can add references to your objects easier.
Cheers!
// composer.json
{
"require": {
"php": "^7.1.3",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
"knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
"knplabs/knp-time-bundle": "^1.8", // 1.8.0
"nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
"php-http/guzzle6-adapter": "^1.1", // v1.1.1
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/orm-pack": "^1.0", // v1.0.6
"symfony/security-bundle": "^4.0", // v4.1.4
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/extensions": "^1.5" // v1.5.2
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
"easycorp/easy-log-handler": "^1.0.2", // v1.0.7
"fzaninotto/faker": "^1.7", // v1.8.0
"symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.0", // v1.7.0
"symfony/monolog-bundle": "^3.0", // v3.3.0
"symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
"symfony/profiler-pack": "^1.0", // v1.0.3
"symfony/var-dumper": "^3.3|^4.0" // v4.1.4
}
}
there there
I also use code started at the first tutorial. I think many of us follow tutorials in different ways, keeping the main ideas, but writing code in our own style, so starting every next tutorial with new code/repo is not a fancy idea.
What I did to overcome your problem cybernet2u is: